Public Access
Исправил постинг токена в ссылках
This commit is contained in:
@@ -1 +1,2 @@
|
|||||||
.env
|
.env
|
||||||
|
go-chat-bridge
|
||||||
|
|||||||
+11
-2
@@ -1,8 +1,15 @@
|
|||||||
# Go Chat Bridge Dockerfile
|
# Go Chat Bridge Dockerfile
|
||||||
FROM golang:1.22-alpine AS builder
|
FROM golang:1.22-alpine AS builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY . .
|
# Сначала только зависимости — этот слой кэшируется, пока не меняются go.mod/go.sum
|
||||||
RUN go mod tidy && go build -o chat-bridge .
|
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
|
FROM alpine:latest
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
@@ -19,5 +26,7 @@ ENV VK_GROUP_ID=""
|
|||||||
ENV SITE_API_URL=""
|
ENV SITE_API_URL=""
|
||||||
ENV SITE_API_TOKEN=""
|
ENV SITE_API_TOKEN=""
|
||||||
ENV WEBHOOK_API_KEY=""
|
ENV WEBHOOK_API_KEY=""
|
||||||
|
ENV PUBLIC_URL=""
|
||||||
|
ENV MEDIA_PROXY_SECRET=""
|
||||||
|
|
||||||
CMD ["./chat-bridge"]
|
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
|
# Go Chat Bridge
|
||||||
|
|
||||||
Мультичат-бот для объединения Telegram, Discord и VK.
|
Мультичат-бот для объединения чатов в **Telegram**, **Discord**, **VK** и на **вашем сайте**.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
- Пересылка сообщений между Telegram, Discord и VK
|
|
||||||
- Отображение имени отправителя в пересылаемых сообщениях
|
- Пересылка сообщений между Telegram, Discord, VK и сайтом
|
||||||
- Поддержка работы с группами VK через LongPoll
|
- Отображение имени отправителя в пересылаемых сообщениях (`[TG]`, `[DS]`, `[VK]`, `[SITE]`)
|
||||||
|
- **Медиа**: фото, документы, видео, аудио, голосовые, GIF
|
||||||
|
- В **Discord** и **Telegram** медиа отправляются как вложения (файлы)
|
||||||
|
- В **VK** и на **сайт** — ссылками на прокси (без токена бота в URL)
|
||||||
|
- **Медиа-прокси**: ссылки из Telegram не содержат токен бота; файлы отдаются через ваш сервер
|
||||||
|
- **Webhook** для приёма сообщений с сайта (POST `/webhook`)
|
||||||
|
- Загрузка переменных из файла `.env` при запуске
|
||||||
|
- Версия и дата сборки в логах при старте
|
||||||
|
|
||||||
## Быстрый старт
|
## Быстрый старт
|
||||||
|
|
||||||
1. Клонируйте репозиторий:
|
### 1. Клонирование и зависимости
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone <repo_url>
|
git clone <repo_url>
|
||||||
cd turn-bridge
|
cd turn-bridge
|
||||||
```
|
|
||||||
2. Установите зависимости:
|
|
||||||
```bash
|
|
||||||
go mod tidy
|
go mod tidy
|
||||||
```
|
```
|
||||||
3. Создайте и настройте ботов/токены для всех платформ:
|
|
||||||
- Telegram: получите токен у @BotFather
|
### 2. Токены и ID
|
||||||
- Discord: создайте приложение и получите токен
|
|
||||||
- VK: получите токен группы с правами messages, groups (см. инструкцию ниже)
|
- **Telegram**: токен у [@BotFather](https://t.me/BotFather), ID чата/канала
|
||||||
4. Укажите переменные окружения:
|
- **Discord**: токен приложения, ID канала
|
||||||
```bash
|
- **VK**: токен группы с правами `messages`, `groups` (см. раздел ниже), `VK_PEER_ID` беседы, `VK_GROUP_ID`
|
||||||
export TELEGRAM_TOKEN=... # токен Telegram-бота
|
|
||||||
export DISCORD_TOKEN=... # токен Discord-бота
|
### 3. Переменные окружения
|
||||||
export VK_TOKEN=... # токен группы VK
|
|
||||||
export TELEGRAM_CHAT_ID=... # id чата/канала Telegram
|
Создайте `.env` в корне проекта (при запуске бинарника переменные подхватываются автоматически):
|
||||||
export DISCORD_CHANNEL_ID=... # id канала Discord
|
|
||||||
export VK_PEER_ID=... # peer_id беседы VK
|
```env
|
||||||
export VK_GROUP_ID=... # id группы VK
|
# Обязательные
|
||||||
|
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=длинная-секретная-строка
|
||||||
```
|
```
|
||||||
5. Запустите приложение:
|
|
||||||
|
- **PUBLIC_URL** — базовый URL, по которому доступен этот сервис (порт 8080). По нему отдаются ссылки вида `PUBLIC_URL/media/...`.
|
||||||
|
- **MEDIA_PROXY_SECRET** — секрет для подписи ссылок на медиа. Без него прокси отключён, в VK и на сайт пойдут прямые ссылки Telegram (с токеном бота).
|
||||||
|
|
||||||
|
### 4. Запуск
|
||||||
|
|
||||||
|
**Локально:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
./go-chat-bridge
|
||||||
|
# или
|
||||||
go run .
|
go run .
|
||||||
```
|
```
|
||||||
|
|
||||||
## Получение токена VK с нужными правами
|
**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 и разрешите сообщения.
|
1. В настройках группы VK включите Long Poll API и разрешите сообщения.
|
||||||
2. Получите токен через интерфейс группы или через OAuth:
|
2. Создайте Standalone-приложение VK, получите токен по ссылке:
|
||||||
- Создайте 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
|
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` (или сообщение, что файл не найден)
|
||||||
|
- Версия и дата сборки (при сборке через Docker — дата сборки образа)
|
||||||
Создайте файл `.env` в корне проекта и заполните:
|
- Состояние медиа-прокси (включён / выключен)
|
||||||
|
- Служебные сообщения `[Telegram]`, `[Media]`, `[Bridge]` при пересылке и отдаче медиа
|
||||||
```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
|
|
||||||
- Поддержка вложений и файлов
|
|
||||||
- Админ-команды
|
|
||||||
- Логирование событий
|
|
||||||
|
|
||||||
## Лицензия
|
## Лицензия
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/base64"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
@@ -81,6 +84,58 @@ func NewBridge(telegramToken, discordToken, vkToken string) (*Bridge, error) {
|
|||||||
}, nil
|
}, 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
|
// Отправка сообщения в Telegram
|
||||||
func (b *Bridge) SendToTelegram(chatID int64, text string) error {
|
func (b *Bridge) SendToTelegram(chatID int64, text string) error {
|
||||||
msg := tgbotapi.NewMessage(chatID, text)
|
msg := tgbotapi.NewMessage(chatID, text)
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ services:
|
|||||||
SITE_API_URL: "${SITE_API_URL}"
|
SITE_API_URL: "${SITE_API_URL}"
|
||||||
SITE_API_TOKEN: "${SITE_API_TOKEN}"
|
SITE_API_TOKEN: "${SITE_API_TOKEN}"
|
||||||
WEBHOOK_API_KEY: "${WEBHOOK_API_KEY}"
|
WEBHOOK_API_KEY: "${WEBHOOK_API_KEY}"
|
||||||
|
PUBLIC_URL: "${PUBLIC_URL}"
|
||||||
|
MEDIA_PROXY_SECRET: "${MEDIA_PROXY_SECRET}"
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
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/klauspost/compress v1.17.9 // indirect
|
||||||
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect
|
||||||
github.com/vmihailenco/tagparser/v2 v2.0.0 // 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.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 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
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 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
|
||||||
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
|||||||
@@ -2,15 +2,35 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Заполняются при сборке: go build -ldflags "-X main.Version=... -X main.BuildDate=..."
|
||||||
|
var (
|
||||||
|
Version = "dev"
|
||||||
|
BuildDate = "unknown"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
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.Println("Go Chat Bridge запущен.")
|
||||||
|
log.Printf("Версия: %s, сборка: %s", Version, BuildDate)
|
||||||
|
|
||||||
telegramToken := os.Getenv("TELEGRAM_TOKEN")
|
telegramToken := os.Getenv("TELEGRAM_TOKEN")
|
||||||
discordToken := os.Getenv("DISCORD_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.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) })
|
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
|
// webhook endpoint
|
||||||
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/webhook", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
@@ -193,6 +274,11 @@ func main() {
|
|||||||
w.Write([]byte("ok"))
|
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))
|
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
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) {
|
getFileURL := func(fileID string) (string, error) {
|
||||||
file, err := b.Telegram.GetFile(tgbotapi.FileConfig{FileID: fileID})
|
file, err := b.Telegram.GetFile(tgbotapi.FileConfig{FileID: fileID})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[Telegram] GetFile error: %v", err)
|
||||||
return "", 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