Public Access
Исправил постинг токена в ссылках
This commit is contained in:
@@ -1 +1,2 @@
|
||||
.env
|
||||
go-chat-bridge
|
||||
|
||||
+11
-2
@@ -1,8 +1,15 @@
|
||||
# Go Chat Bridge Dockerfile
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN go mod tidy && go build -o chat-bridge .
|
||||
# Сначала только зависимости — этот слой кэшируется, пока не меняются go.mod/go.sum
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
# Код копируем отдельно — любое изменение .go пересобирает бинарник
|
||||
# Версию можно задать: docker build --build-arg VERSION=1.0.0
|
||||
ARG VERSION=dev
|
||||
COPY *.go ./
|
||||
RUN BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ) && \
|
||||
go build -ldflags "-X main.Version=${VERSION} -X main.BuildDate=$BUILD_DATE" -o chat-bridge .
|
||||
|
||||
FROM alpine:latest
|
||||
WORKDIR /app
|
||||
@@ -19,5 +26,7 @@ ENV VK_GROUP_ID=""
|
||||
ENV SITE_API_URL=""
|
||||
ENV SITE_API_TOKEN=""
|
||||
ENV WEBHOOK_API_KEY=""
|
||||
ENV PUBLIC_URL=""
|
||||
ENV MEDIA_PROXY_SECRET=""
|
||||
|
||||
CMD ["./chat-bridge"]
|
||||
|
||||
@@ -1,99 +1,117 @@
|
||||
## Пример .env
|
||||
|
||||
Создайте файл `.env` в корне проекта и заполните:
|
||||
|
||||
```env
|
||||
TELEGRAM_TOKEN=your_telegram_token
|
||||
DISCORD_TOKEN=your_discord_token
|
||||
VK_TOKEN=your_vk_token
|
||||
TELEGRAM_CHAT_ID=-1001234567890
|
||||
DISCORD_CHANNEL_ID=123456789012345678
|
||||
VK_PEER_ID=2000000001
|
||||
VK_GROUP_ID=2000000001
|
||||
```
|
||||
|
||||
docker-compose автоматически подхватит эти переменные.
|
||||
|
||||
# Go Chat Bridge
|
||||
|
||||
Мультичат-бот для объединения Telegram, Discord и VK.
|
||||
Мультичат-бот для объединения чатов в **Telegram**, **Discord**, **VK** и на **вашем сайте**.
|
||||
|
||||
## Возможности
|
||||
- Пересылка сообщений между Telegram, Discord и VK
|
||||
- Отображение имени отправителя в пересылаемых сообщениях
|
||||
- Поддержка работы с группами VK через LongPoll
|
||||
|
||||
- Пересылка сообщений между Telegram, Discord, VK и сайтом
|
||||
- Отображение имени отправителя в пересылаемых сообщениях (`[TG]`, `[DS]`, `[VK]`, `[SITE]`)
|
||||
- **Медиа**: фото, документы, видео, аудио, голосовые, GIF
|
||||
- В **Discord** и **Telegram** медиа отправляются как вложения (файлы)
|
||||
- В **VK** и на **сайт** — ссылками на прокси (без токена бота в URL)
|
||||
- **Медиа-прокси**: ссылки из Telegram не содержат токен бота; файлы отдаются через ваш сервер
|
||||
- **Webhook** для приёма сообщений с сайта (POST `/webhook`)
|
||||
- Загрузка переменных из файла `.env` при запуске
|
||||
- Версия и дата сборки в логах при старте
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
1. Клонируйте репозиторий:
|
||||
```bash
|
||||
git clone <repo_url>
|
||||
cd turn-bridge
|
||||
```
|
||||
2. Установите зависимости:
|
||||
```bash
|
||||
go mod tidy
|
||||
```
|
||||
3. Создайте и настройте ботов/токены для всех платформ:
|
||||
- Telegram: получите токен у @BotFather
|
||||
- Discord: создайте приложение и получите токен
|
||||
- VK: получите токен группы с правами messages, groups (см. инструкцию ниже)
|
||||
4. Укажите переменные окружения:
|
||||
```bash
|
||||
export TELEGRAM_TOKEN=... # токен Telegram-бота
|
||||
export DISCORD_TOKEN=... # токен Discord-бота
|
||||
export VK_TOKEN=... # токен группы VK
|
||||
export TELEGRAM_CHAT_ID=... # id чата/канала Telegram
|
||||
export DISCORD_CHANNEL_ID=... # id канала Discord
|
||||
export VK_PEER_ID=... # peer_id беседы VK
|
||||
export VK_GROUP_ID=... # id группы VK
|
||||
```
|
||||
5. Запустите приложение:
|
||||
```bash
|
||||
go run .
|
||||
```
|
||||
### 1. Клонирование и зависимости
|
||||
|
||||
## Получение токена VK с нужными правами
|
||||
```bash
|
||||
git clone <repo_url>
|
||||
cd turn-bridge
|
||||
go mod tidy
|
||||
```
|
||||
|
||||
### 2. Токены и ID
|
||||
|
||||
- **Telegram**: токен у [@BotFather](https://t.me/BotFather), ID чата/канала
|
||||
- **Discord**: токен приложения, ID канала
|
||||
- **VK**: токен группы с правами `messages`, `groups` (см. раздел ниже), `VK_PEER_ID` беседы, `VK_GROUP_ID`
|
||||
|
||||
### 3. Переменные окружения
|
||||
|
||||
Создайте `.env` в корне проекта (при запуске бинарника переменные подхватываются автоматически):
|
||||
|
||||
```env
|
||||
# Обязательные
|
||||
TELEGRAM_TOKEN=...
|
||||
DISCORD_TOKEN=...
|
||||
VK_TOKEN=...
|
||||
|
||||
# ID чатов
|
||||
TELEGRAM_CHAT_ID=-1001234567890
|
||||
DISCORD_CHANNEL_ID=123456789012345678
|
||||
VK_PEER_ID=2000000002
|
||||
VK_GROUP_ID=15469861
|
||||
|
||||
# Сайт: отправка сообщений на сайт и приём с сайта
|
||||
SITE_API_URL=https://your-site.com/telebot/chat_api.php
|
||||
SITE_API_TOKEN=...
|
||||
WEBHOOK_API_KEY=...
|
||||
|
||||
# Медиа-прокси (чтобы в ссылках для VK и сайта не светить токен Telegram)
|
||||
PUBLIC_URL=https://your-bridge-host:8080
|
||||
MEDIA_PROXY_SECRET=длинная-секретная-строка
|
||||
```
|
||||
|
||||
- **PUBLIC_URL** — базовый URL, по которому доступен этот сервис (порт 8080). По нему отдаются ссылки вида `PUBLIC_URL/media/...`.
|
||||
- **MEDIA_PROXY_SECRET** — секрет для подписи ссылок на медиа. Без него прокси отключён, в VK и на сайт пойдут прямые ссылки Telegram (с токеном бота).
|
||||
|
||||
### 4. Запуск
|
||||
|
||||
**Локально:**
|
||||
|
||||
```bash
|
||||
./go-chat-bridge
|
||||
# или
|
||||
go run .
|
||||
```
|
||||
|
||||
**Docker:**
|
||||
|
||||
```bash
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
При сборке образа можно задать версию:
|
||||
`docker build --build-arg VERSION=1.0.0 -t chat-bridge .`
|
||||
|
||||
## Эндпоинты (порт 8080)
|
||||
|
||||
| Путь | Описание |
|
||||
|-----------|----------|
|
||||
| `POST /webhook` | Приём сообщений с сайта. Параметр `key` (или поле `key` в JSON) = `WEBHOOK_API_KEY`; в теле — `text`, `sender`. |
|
||||
| `GET /media/<токен>/<имя_файла>.<расширение>` | Прокси медиа из Telegram. Токен подписан, расширение в URL для корректного отображения и сохранения. |
|
||||
|
||||
## Формат сообщений в чатах
|
||||
|
||||
- `[TG] Имя: текст` — из Telegram
|
||||
- `[DS] Имя: текст` — из Discord
|
||||
- `[VK] Имя: текст` — из VK
|
||||
- `[SITE] Имя: текст` — с сайта
|
||||
|
||||
При наличии медиа к сообщению добавляются ссылки (в VK и на сайте — на прокси с расширением в конце, например `.../media/xxx/file_332.jpg`).
|
||||
|
||||
## Получение токена VK
|
||||
|
||||
1. В настройках группы VK включите Long Poll API и разрешите сообщения.
|
||||
2. Получите токен через интерфейс группы или через OAuth:
|
||||
- Создайте Standalone-приложение VK
|
||||
- Перейдите по ссылке:
|
||||
2. Создайте Standalone-приложение VK, получите токен по ссылке:
|
||||
```
|
||||
https://oauth.vk.com/authorize?client_id=ВАШ_CLIENT_ID&display=page&redirect_uri=https://oauth.vk.com/blank.html&scope=messages,groups&response_type=token&v=5.131
|
||||
```
|
||||
- Авторизуйтесь, скопируйте access_token из адресной строки.
|
||||
Скопируйте `access_token` из адресной строки после редиректа.
|
||||
|
||||
## Формат пересылаемых сообщений
|
||||
## Логи
|
||||
|
||||
- `[TG] user: текст` — из Telegram
|
||||
- `[DS] user: текст` — из Discord
|
||||
- `[VK] user: текст` — из VK (имя пользователя VK)
|
||||
При старте выводятся:
|
||||
|
||||
## Пример .env
|
||||
|
||||
Создайте файл `.env` в корне проекта и заполните:
|
||||
|
||||
```env
|
||||
TELEGRAM_TOKEN=your_telegram_token
|
||||
DISCORD_TOKEN=your_discord_token
|
||||
VK_TOKEN=your_vk_token
|
||||
TELEGRAM_CHAT_ID=-1001234567890
|
||||
DISCORD_CHANNEL_ID=123456789012345678
|
||||
VK_PEER_ID=2000000001
|
||||
VK_GROUP_ID=2000000001
|
||||
SITE_API_URL=https://turn-guild.ru/
|
||||
SITE_API_TOKEN=website_webhook_token
|
||||
WEBHOOK_API_KEY=brige_webhook_token
|
||||
```
|
||||
|
||||
docker-compose автоматически подхватит эти переменные.
|
||||
## TODO
|
||||
- Поддержка вложений и файлов
|
||||
- Админ-команды
|
||||
- Логирование событий
|
||||
- Загрузка `.env` (или сообщение, что файл не найден)
|
||||
- Версия и дата сборки (при сборке через Docker — дата сборки образа)
|
||||
- Состояние медиа-прокси (включён / выключен)
|
||||
- Служебные сообщения `[Telegram]`, `[Media]`, `[Bridge]` при пересылке и отдаче медиа
|
||||
|
||||
## Лицензия
|
||||
|
||||
MIT
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime"
|
||||
@@ -81,6 +84,58 @@ func NewBridge(telegramToken, discordToken, vkToken string) (*Bridge, error) {
|
||||
}, nil
|
||||
}
|
||||
|
||||
// signMediaToken создаёт подписанный токен для filePath (HMAC-SHA256), чтобы не светить токен бота в ссылках.
|
||||
func signMediaToken(filePath, secret string) string {
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(filePath))
|
||||
sig := mac.Sum(nil)
|
||||
// payload.base64(sig) — в URL без padding
|
||||
enc := base64.URLEncoding.WithPadding(base64.NoPadding)
|
||||
return enc.EncodeToString([]byte(filePath)) + "." + enc.EncodeToString(sig)
|
||||
}
|
||||
|
||||
// verifyMediaToken проверяет подпись и возвращает filePath. secret должен совпадать с тем, что при подписи.
|
||||
func verifyMediaToken(token, secret string) (filePath string, ok bool) {
|
||||
enc := base64.URLEncoding.WithPadding(base64.NoPadding)
|
||||
idx := strings.LastIndex(token, ".")
|
||||
if idx == -1 {
|
||||
return "", false
|
||||
}
|
||||
payloadB, err1 := enc.DecodeString(token[:idx])
|
||||
sigB, err2 := enc.DecodeString(token[idx+1:])
|
||||
if err1 != nil || err2 != nil || len(sigB) != sha256.Size {
|
||||
return "", false
|
||||
}
|
||||
filePath = string(payloadB)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(filePath))
|
||||
expected := mac.Sum(nil)
|
||||
if !hmac.Equal(sigB, expected) {
|
||||
return "", false
|
||||
}
|
||||
return filePath, true
|
||||
}
|
||||
|
||||
// GetTelegramFileProxyURL возвращает публичный URL прокси для файла Telegram (без токена в ссылке).
|
||||
// В конец URL добавлено имя файла с расширением, чтобы ссылка в чате показывала .jpg и т.д.
|
||||
func (b *Bridge) GetTelegramFileProxyURL(filePath string) string {
|
||||
publicURL := strings.TrimSuffix(os.Getenv("PUBLIC_URL"), "/")
|
||||
secret := os.Getenv("MEDIA_PROXY_SECRET")
|
||||
if publicURL == "" || secret == "" {
|
||||
return ""
|
||||
}
|
||||
fileName := path.Base(filePath)
|
||||
if fileName == "" || fileName == "." {
|
||||
fileName = "file"
|
||||
}
|
||||
return publicURL + "/media/" + signMediaToken(filePath, secret) + "/" + url.PathEscape(fileName)
|
||||
}
|
||||
|
||||
// GetTelegramFileDirectURL возвращает прямую ссылку на файл Telegram (только для внутреннего использования на сервере).
|
||||
func (b *Bridge) GetTelegramFileDirectURL(filePath string) string {
|
||||
return fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", b.Telegram.Token, filePath)
|
||||
}
|
||||
|
||||
// Отправка сообщения в Telegram
|
||||
func (b *Bridge) SendToTelegram(chatID int64, text string) error {
|
||||
msg := tgbotapi.NewMessage(chatID, text)
|
||||
|
||||
@@ -14,6 +14,8 @@ services:
|
||||
SITE_API_URL: "${SITE_API_URL}"
|
||||
SITE_API_TOKEN: "${SITE_API_TOKEN}"
|
||||
WEBHOOK_API_KEY: "${WEBHOOK_API_KEY}"
|
||||
PUBLIC_URL: "${PUBLIC_URL}"
|
||||
MEDIA_PROXY_SECRET: "${MEDIA_PROXY_SECRET}"
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
|
||||
@@ -10,6 +10,7 @@ require (
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/joho/godotenv v1.5.1 // indirect
|
||||
github.com/klauspost/compress v1.17.9 // indirect
|
||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
|
||||
|
||||
@@ -9,6 +9,8 @@ github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
|
||||
@@ -2,15 +2,35 @@ package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Заполняются при сборке: go build -ldflags "-X main.Version=... -X main.BuildDate=..."
|
||||
var (
|
||||
Version = "dev"
|
||||
BuildDate = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Загружаем .env из текущей директории или рядом с бинарником
|
||||
if err := godotenv.Load(); err != nil {
|
||||
execPath, _ := os.Executable()
|
||||
envPath := filepath.Join(filepath.Dir(execPath), ".env")
|
||||
if err2 := godotenv.Load(envPath); err2 != nil {
|
||||
log.Println("Файл .env не найден, используются переменные окружения shell")
|
||||
}
|
||||
}
|
||||
log.Println("Go Chat Bridge запущен.")
|
||||
log.Printf("Версия: %s, сборка: %s", Version, BuildDate)
|
||||
|
||||
telegramToken := os.Getenv("TELEGRAM_TOKEN")
|
||||
discordToken := os.Getenv("DISCORD_TOKEN")
|
||||
@@ -144,6 +164,67 @@ func main() {
|
||||
bridge.StartDiscordListener(discordChannelID, func(text string, sender string, media []string) { forward(text, "discord", sender, media) })
|
||||
bridge.StartVKListener(func(text string, sender string, media []string) { forward(text, "vk", sender, media) })
|
||||
|
||||
// Прокси медиа Telegram: по подписанному токену отдаём файл без раскрытия токена бота в ссылках
|
||||
http.HandleFunc("/media/", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodHead {
|
||||
log.Printf("[Media] %s %s — Method not allowed", r.Method, r.URL.Path)
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
// Путь: /media/<токен>/<имя_файла.jpg> — имя в конце только для вида, токен до последнего /
|
||||
pathSuffix := strings.TrimPrefix(r.URL.Path, "/media/")
|
||||
if pathSuffix == "" {
|
||||
log.Printf("[Media] запрос без токена")
|
||||
http.Error(w, "Not found", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
token := pathSuffix
|
||||
if idx := strings.LastIndex(pathSuffix, "/"); idx != -1 {
|
||||
token = pathSuffix[:idx]
|
||||
}
|
||||
secret := os.Getenv("MEDIA_PROXY_SECRET")
|
||||
if secret == "" {
|
||||
log.Printf("[Media] MEDIA_PROXY_SECRET не задан, прокси отключён")
|
||||
http.Error(w, "Proxy not configured", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
filePath, ok := verifyMediaToken(token, secret)
|
||||
if !ok {
|
||||
pathLog := r.URL.Path
|
||||
if len(pathLog) > 50 {
|
||||
pathLog = pathLog[:50] + "..."
|
||||
}
|
||||
log.Printf("[Media] неверный или просроченный токен (path=%s)", pathLog)
|
||||
http.Error(w, "Invalid or expired link", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
log.Printf("[Media] отдача файла: %s", filePath)
|
||||
directURL := bridge.GetTelegramFileDirectURL(filePath)
|
||||
resp, err := http.Get(directURL)
|
||||
if err != nil {
|
||||
log.Printf("[Media] ошибка загрузки из Telegram для %s: %v", filePath, err)
|
||||
http.Error(w, "Failed to fetch file", http.StatusBadGateway)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
log.Printf("[Media] Telegram вернул %d для %s", resp.StatusCode, filePath)
|
||||
http.Error(w, "Upstream error", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
if ct := resp.Header.Get("Content-Type"); ct != "" {
|
||||
w.Header().Set("Content-Type", ct)
|
||||
}
|
||||
// Имя файла с расширением — иначе при открытии/сохранении расширение теряется
|
||||
fileName := path.Base(filePath)
|
||||
if fileName != "" && fileName != "." {
|
||||
w.Header().Set("Content-Disposition", "inline; filename=\""+fileName+"\"")
|
||||
}
|
||||
w.Header().Set("Cache-Control", "private, max-age=86400")
|
||||
io.Copy(w, resp.Body)
|
||||
log.Printf("[Media] отдано: %s", filePath)
|
||||
})
|
||||
|
||||
// webhook endpoint
|
||||
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
@@ -193,6 +274,11 @@ func main() {
|
||||
w.Write([]byte("ok"))
|
||||
})
|
||||
|
||||
log.Println("Webhook listening on :8080 (/webhook)")
|
||||
if os.Getenv("PUBLIC_URL") != "" && os.Getenv("MEDIA_PROXY_SECRET") != "" {
|
||||
log.Println("Медиа-прокси включён: ссылки из Telegram будут без токена в URL")
|
||||
} else {
|
||||
log.Println("Медиа-прокси выключен (задайте PUBLIC_URL и MEDIA_PROXY_SECRET для включения)")
|
||||
}
|
||||
log.Println("Webhook listening on :8080 (/webhook, /media/)")
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
@@ -29,9 +28,15 @@ func (b *Bridge) StartTelegramListener(forward func(text string, sender string,
|
||||
getFileURL := func(fileID string) (string, error) {
|
||||
file, err := b.Telegram.GetFile(tgbotapi.FileConfig{FileID: fileID})
|
||||
if err != nil {
|
||||
log.Printf("[Telegram] GetFile error: %v", err)
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("https://api.telegram.org/file/bot%s/%s", b.Telegram.Token, file.FilePath), nil
|
||||
if proxyURL := b.GetTelegramFileProxyURL(file.FilePath); proxyURL != "" {
|
||||
log.Printf("[Telegram] медиа: прокси-ссылка для %s", file.FilePath)
|
||||
return proxyURL, nil
|
||||
}
|
||||
log.Printf("[Telegram] медиа: прямая ссылка (PUBLIC_URL/MEDIA_PROXY_SECRET не заданы?) для %s", file.FilePath)
|
||||
return b.GetTelegramFileDirectURL(file.FilePath), nil
|
||||
}
|
||||
|
||||
// Фото (берём самое большое)
|
||||
|
||||
Reference in New Issue
Block a user