diff --git a/bridge.go b/bridge.go index c86430d..b19e905 100644 --- a/bridge.go +++ b/bridge.go @@ -1,16 +1,21 @@ package main import ( - "strconv" - tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" - "github.com/bwmarrin/discordgo" - "github.com/SevereCloud/vksdk/v2/api" - "net/http" - "net/url" "fmt" "io" + "mime" + "net/http" + "net/url" "os" + "path" + "strconv" + "strings" + + "github.com/SevereCloud/vksdk/v2/api" + "github.com/bwmarrin/discordgo" + tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5" ) + // Отправка сообщения на сайт func (b *Bridge) SendToSite(text string) error { apiURL := os.Getenv("SITE_API_URL") @@ -36,6 +41,7 @@ func (b *Bridge) SendToSite(text string) error { _, err = io.ReadAll(resp.Body) return err } + // Получить имя пользователя VK по id func (b *Bridge) GetVKUserName(userID int) (string, error) { resp, err := b.VK.UsersGet(api.Params{"user_ids": userID}) @@ -75,25 +81,161 @@ func NewBridge(telegramToken, discordToken, vkToken string) (*Bridge, error) { }, nil } - // Отправка сообщения в Telegram - func (b *Bridge) SendToTelegram(chatID int64, text string) error { - msg := tgbotapi.NewMessage(chatID, text) - _, err := b.Telegram.Send(msg) - return err - } +// Отправка сообщения в Telegram +func (b *Bridge) SendToTelegram(chatID int64, text string) error { + msg := tgbotapi.NewMessage(chatID, text) + _, err := b.Telegram.Send(msg) + return err +} - // Отправка сообщения в Discord - func (b *Bridge) SendToDiscord(channelID, text string) error { - _, err := b.Discord.ChannelMessageSend(channelID, text) - return err - } +// Отправка сообщения в Discord +func (b *Bridge) SendToDiscord(channelID, text string) error { + _, err := b.Discord.ChannelMessageSend(channelID, text) + return err +} - // Отправка сообщения в VK - func (b *Bridge) SendToVK(peerID int, text string) error { - _, err := b.VK.MessagesSend(api.Params{ - "peer_id": peerID, - "message": text, - "random_id": 0, - }) - return err +// Отправка сообщения в VK +func (b *Bridge) SendToVK(peerID int, text string) error { + _, err := b.VK.MessagesSend(api.Params{ + "peer_id": peerID, + "message": text, + "random_id": 0, + }) + return err +} + +// Загруженный файл на диск для пересылки как вложение +type DownloadedFile struct { + FileName string + TempPath string + Mime string +} + +// DownloadFiles скачивает файлы по URL и сохраняет во временные файлы +func (b *Bridge) DownloadFiles(urls []string) ([]DownloadedFile, error) { + results := make([]DownloadedFile, 0, len(urls)) + for _, u := range urls { + resp, err := http.Get(u) + if err != nil { + return results, err + } + if resp.Body != nil { + defer resp.Body.Close() + } + + parsed, err := url.Parse(u) + if err != nil { + return results, err + } + name := path.Base(parsed.Path) + if name == "" || name == "/" || name == "." { + name = "file" + } + + ext := path.Ext(name) + pattern := "bridge_*" + ext + + tmpFile, err := os.CreateTemp("", pattern) + if err != nil { + return results, err + } + _, copyErr := io.Copy(tmpFile, resp.Body) + closeErr := tmpFile.Close() + if copyErr != nil { + _ = os.Remove(tmpFile.Name()) + return results, copyErr + } + if closeErr != nil { + _ = os.Remove(tmpFile.Name()) + return results, closeErr + } + + // Try to determine mime: header -> by extension -> sniff file + contentType := resp.Header.Get("Content-Type") + if contentType == "" || contentType == "application/octet-stream" { + if m := mime.TypeByExtension(ext); m != "" { + contentType = m + } + } + if contentType == "" || contentType == "application/octet-stream" { + if f2, err := os.Open(tmpFile.Name()); err == nil { + buf := make([]byte, 512) + n, _ := f2.Read(buf) + _ = f2.Close() + if n > 0 { + contentType = http.DetectContentType(buf[:n]) + } + } + } + + results = append(results, DownloadedFile{FileName: name, TempPath: tmpFile.Name(), Mime: contentType}) } + return results, nil +} + +// CleanupFiles удаляет временные файлы +func (b *Bridge) CleanupFiles(files []DownloadedFile) { + for _, f := range files { + _ = os.Remove(f.TempPath) + } +} + +// SendFilesToDiscord отправляет один или несколько файлов в Discord канал +func (b *Bridge) SendFilesToDiscord(channelID string, files []DownloadedFile, content string) error { + dgFiles := make([]*discordgo.File, 0, len(files)) + openFiles := make([]*os.File, 0, len(files)) + for _, f := range files { + fh, err := os.Open(f.TempPath) + if err != nil { + for _, of := range openFiles { + _ = of.Close() + } + return err + } + openFiles = append(openFiles, fh) + dgFiles = append(dgFiles, &discordgo.File{Name: f.FileName, Reader: fh}) + } + _, err := b.Discord.ChannelMessageSendComplex(channelID, &discordgo.MessageSend{Content: content, Files: dgFiles}) + for _, of := range openFiles { + _ = of.Close() + } + return err +} + +// SendFilesToTelegram отправляет файлы как документы, caption добавляется только к первому +func (b *Bridge) SendFilesToTelegram(chatID int64, files []DownloadedFile, caption string) error { + for i, f := range files { + fh, err := os.Open(f.TempPath) + if err != nil { + return err + } + var sendErr error + if strings.HasPrefix(f.Mime, "image/") { + media := tgbotapi.NewPhoto(chatID, tgbotapi.FileReader{Name: f.FileName, Reader: fh}) + if i == 0 && caption != "" { + media.Caption = caption + } + _, sendErr = b.Telegram.Send(media) + } else if strings.HasPrefix(f.Mime, "video/") { + media := tgbotapi.NewVideo(chatID, tgbotapi.FileReader{Name: f.FileName, Reader: fh}) + if i == 0 && caption != "" { + media.Caption = caption + } + _, sendErr = b.Telegram.Send(media) + } else { + media := tgbotapi.NewDocument(chatID, tgbotapi.FileReader{Name: f.FileName, Reader: fh}) + if i == 0 && caption != "" { + media.Caption = caption + } + _, sendErr = b.Telegram.Send(media) + } + closeErr := fh.Close() + if sendErr != nil { + return sendErr + } + if closeErr != nil { + return closeErr + } + } + return nil +} diff --git a/main.go b/main.go index 741981c..9ba31f1 100644 --- a/main.go +++ b/main.go @@ -45,9 +45,21 @@ func main() { switch from { case "telegram": + // Для Discord отправляем файлы, если есть, иначе текст if discordChannelID != "" { - _ = bridge.SendToDiscord(discordChannelID, "[TG] "+sender+": "+msg) + if len(media) > 0 { + files, derr := bridge.DownloadFiles(media) + if derr == nil && len(files) > 0 { + _ = bridge.SendFilesToDiscord(discordChannelID, files, "[TG] "+sender+": "+text) + bridge.CleanupFiles(files) + } else { + _ = bridge.SendToDiscord(discordChannelID, "[TG] "+sender+": "+msg) + } + } else { + _ = bridge.SendToDiscord(discordChannelID, "[TG] "+sender+": "+msg) + } } + // Для VK и сайта оставляем ссылки if vkPeerID != "" { if id, err := strconv.Atoi(vkPeerID); err == nil { _ = bridge.SendToVK(id, "[TG] "+sender+": "+msg) @@ -55,11 +67,23 @@ func main() { } _ = bridge.SendToSite("[TG] " + sender + ": " + msg) case "discord": + // Для Telegram отправляем файлы, если есть, иначе текст if telegramChatID != "" { if id, err := strconv.ParseInt(telegramChatID, 10, 64); err == nil { - _ = bridge.SendToTelegram(id, "[DS] "+sender+": "+msg) + if len(media) > 0 { + files, derr := bridge.DownloadFiles(media) + if derr == nil && len(files) > 0 { + _ = bridge.SendFilesToTelegram(id, files, "[DS] "+sender+": "+text) + bridge.CleanupFiles(files) + } else { + _ = bridge.SendToTelegram(id, "[DS] "+sender+": "+msg) + } + } else { + _ = bridge.SendToTelegram(id, "[DS] "+sender+": "+msg) + } } } + // Для VK и сайта оставляем ссылки if vkPeerID != "" { if id, err := strconv.Atoi(vkPeerID); err == nil { _ = bridge.SendToVK(id, "[DS] "+sender+": "+msg) @@ -67,16 +91,39 @@ func main() { } _ = bridge.SendToSite("[DS] " + sender + ": " + msg) case "vk": + // Если когда-либо будут передаваться ссылки на медиа из VK, для TG/Discord можем тоже отправлять файлы if telegramChatID != "" { if id, err := strconv.ParseInt(telegramChatID, 10, 64); err == nil { - _ = bridge.SendToTelegram(id, "[VK] "+sender+": "+msg) + if len(media) > 0 { + files, derr := bridge.DownloadFiles(media) + if derr == nil && len(files) > 0 { + _ = bridge.SendFilesToTelegram(id, files, "[VK] "+sender+": "+text) + bridge.CleanupFiles(files) + } else { + _ = bridge.SendToTelegram(id, "[VK] "+sender+": "+msg) + } + } else { + _ = bridge.SendToTelegram(id, "[VK] "+sender+": "+msg) + } } } if discordChannelID != "" { - _ = bridge.SendToDiscord(discordChannelID, "[VK] "+sender+": "+msg) + if len(media) > 0 { + files, derr := bridge.DownloadFiles(media) + if derr == nil && len(files) > 0 { + _ = bridge.SendFilesToDiscord(discordChannelID, files, "[VK] "+sender+": "+text) + bridge.CleanupFiles(files) + } else { + _ = bridge.SendToDiscord(discordChannelID, "[VK] "+sender+": "+msg) + } + } else { + _ = bridge.SendToDiscord(discordChannelID, "[VK] "+sender+": "+msg) + } } + // Для сайта оставляем ссылки _ = bridge.SendToSite("[VK] " + sender + ": " + msg) case "site": + // Из сайта пока не принимаем ссылки на файлы, отправляем как текст if telegramChatID != "" { if id, err := strconv.ParseInt(telegramChatID, 10, 64); err == nil { _ = bridge.SendToTelegram(id, "[SITE] "+sender+": "+msg)