commit 7c610d04771cb1b0916b2e12e49dd3e90b5e7d25 Author: nquidox Date: Sun Mar 23 15:35:24 2025 +0300 initial diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..485dee6 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.idea diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000..9d621c2 --- /dev/null +++ b/config/config.go @@ -0,0 +1,69 @@ +package config + +import ( + "github.com/disgoorg/snowflake/v2" + log "github.com/sirupsen/logrus" + "os" + "strconv" +) + +type Config struct { + AppConf AppConfig + TgConf TelegramConfig + DsConf DiscordConfig +} + +type AppConfig struct { + LogLvl string +} + +type TelegramConfig struct { + Token string + ChatID int64 +} + +type DiscordConfig struct { + Token string + GuildID snowflake.ID + ChannelID snowflake.ID +} + +func NewConfig() *Config { + return &Config{ + AppConfig{ + LogLvl: "info", + }, + TelegramConfig{ + Token: getEnv("TELEGRAM_TOKEN", ""), + ChatID: convertChatID(getEnv("TELEGRAM_CHANNEL_ID", "")), + }, + DiscordConfig{ + Token: getEnv("DISCORD_TOKEN", ""), + GuildID: convertID(getEnv("GUILD_ID", "")), + ChannelID: convertID(getEnv("CHANNEL_ID", "")), + }, + } +} + +func getEnv(key, fallback string) string { + if value, ok := os.LookupEnv(key); ok { + return value + } + return fallback +} + +func convertID(channelIDStr string) snowflake.ID { + channelIDUint, err := strconv.ParseUint(channelIDStr, 10, 64) + if err != nil { + log.Fatal("Cannot convert channel ID to snowflake ID") + } + return snowflake.ID(channelIDUint) +} + +func convertChatID(str string) int64 { + id, err := strconv.ParseInt(str, 10, 64) + if err != nil { + log.Fatal("Cannot convert string to int64") + } + return id +} diff --git a/config/logging.go b/config/logging.go new file mode 100644 index 0000000..5c8809c --- /dev/null +++ b/config/logging.go @@ -0,0 +1,35 @@ +package config + +import ( + "fmt" + log "github.com/sirupsen/logrus" + "os" + "path" + "runtime" +) + +func LogSetup(lvl string) { + l, err := log.ParseLevel(lvl) + if err != nil { + log.SetLevel(log.DebugLevel) + } + + log.SetFormatter( + &log.TextFormatter{ + FullTimestamp: true, + CallerPrettyfier: func(f *runtime.Frame) (string, string) { + filename := path.Base(f.File) + return fmt.Sprintf("%s()", f.Function), fmt.Sprintf(" %s:%d", filename, f.Line) + }, + }, + ) + + if l == log.DebugLevel { + log.SetLevel(l) + log.SetReportCaller(true) + } else { + log.SetLevel(l) + } + + log.SetOutput(os.Stdout) +} diff --git a/discordBot/handler.go b/discordBot/handler.go new file mode 100644 index 0000000..6c5c95d --- /dev/null +++ b/discordBot/handler.go @@ -0,0 +1,69 @@ +package discordBot + +import ( + "context" + "fmt" + "github.com/disgoorg/disgo" + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/discord" + "github.com/disgoorg/disgo/gateway" + "github.com/disgoorg/snowflake/v2" + log "github.com/sirupsen/logrus" + "tg-disc-bot/config" + "tg-disc-bot/dto" +) + +type DiscordBot struct { + ctx context.Context + client bot.Client + guildID snowflake.ID + channelID snowflake.ID +} + +func NewDiscordBot(ctx context.Context, config config.DiscordConfig) (*DiscordBot, error) { + var err error + dsBot := &DiscordBot{ctx: ctx, channelID: config.ChannelID} + + dsBot.client, err = disgo.New(config.Token, + bot.WithGatewayConfigOpts( + gateway.WithIntents( + gateway.IntentGuilds, + gateway.IntentGuildMessages, + gateway.IntentDirectMessages, + gateway.IntentMessageContent, + ), + ), + ) + if err != nil { + return nil, err + } + return dsBot, nil +} + +func (d *DiscordBot) Start(fromTelegram chan dto.TelegramDTO) chan dto.DiscordDTO { + log.Info("Starting discord bot...") + + msgChan := make(chan dto.DiscordDTO, 100) + d.client.AddEventListeners(&messageHandler{msgChan: msgChan}) + + go func() { + for msg := range fromTelegram { + log.WithField("content", msg).Debug("DS | Message from Telegram") + + m := discord.MessageCreate{Content: fmt.Sprintf("[%s]\n%s", msg.AuthorName, msg.Content)} + + _, err := d.client.Rest().CreateMessage(d.channelID, m) + if err != nil { + log.Errorf("Failed to send message to Discord: %v", err) + } + } + }() + + go func() { + if err := d.client.OpenGateway(d.ctx); err != nil { + log.Fatalf("Failed to open discord gateway %v", err) + } + }() + + return msgChan +} diff --git a/discordBot/messages.go b/discordBot/messages.go new file mode 100644 index 0000000..0322a30 --- /dev/null +++ b/discordBot/messages.go @@ -0,0 +1,28 @@ +package discordBot + +import ( + "github.com/disgoorg/disgo/bot" + "github.com/disgoorg/disgo/events" + log "github.com/sirupsen/logrus" + "tg-disc-bot/dto" +) + +type messageHandler struct { + msgChan chan dto.DiscordDTO +} + +func (m *messageHandler) OnEvent(event bot.Event) { + if e, ok := event.(*events.MessageCreate); ok { + if !e.Message.Author.Bot { + message := dto.DiscordDTO{ + AuthorName: e.Message.Author.Username, + Content: e.Message.Content, + } + + m.msgChan <- message + } else { + log.Debug("DS | Bot message, skipping") + } + + } +} diff --git a/dto/discord.go b/dto/discord.go new file mode 100644 index 0000000..ade3dbf --- /dev/null +++ b/dto/discord.go @@ -0,0 +1,6 @@ +package dto + +type DiscordDTO struct { + AuthorName string + Content string +} diff --git a/dto/telegram.go b/dto/telegram.go new file mode 100644 index 0000000..0fe71b2 --- /dev/null +++ b/dto/telegram.go @@ -0,0 +1,6 @@ +package dto + +type TelegramDTO struct { + AuthorName string + Content string +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f369d5a --- /dev/null +++ b/go.mod @@ -0,0 +1,18 @@ +module tg-disc-bot + +go 1.24.1 + +require ( + github.com/disgoorg/disgo v0.18.15 + github.com/disgoorg/snowflake/v2 v2.0.3 + github.com/go-telegram/bot v1.14.1 + github.com/sirupsen/logrus v1.9.3 +) + +require ( + github.com/disgoorg/json v1.2.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/sys v0.31.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7eb1b36 --- /dev/null +++ b/go.sum @@ -0,0 +1,32 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/disgoorg/disgo v0.18.15 h1:T24I/NdUUody4FDvb8YkhSxHtsgRKD8Ui5Vi5PXnIrQ= +github.com/disgoorg/disgo v0.18.15/go.mod h1:dXYVH059d6aK7mI+Nh/3svSRWedNd09P7C2VX3RqbJY= +github.com/disgoorg/json v1.2.0 h1:6e/j4BCfSHIvucG1cd7tJPAOp1RgnnMFSqkvZUtEd1Y= +github.com/disgoorg/json v1.2.0/go.mod h1:BHDwdde0rpQFDVsRLKhma6Y7fTbQKub/zdGO5O9NqqA= +github.com/disgoorg/snowflake/v2 v2.0.3 h1:3B+PpFjr7j4ad7oeJu4RlQ+nYOTadsKapJIzgvSI2Ro= +github.com/disgoorg/snowflake/v2 v2.0.3/go.mod h1:W6r7NUA7DwfZLwr00km6G4UnZ0zcoLBRufhkFWgAc4c= +github.com/go-telegram/bot v1.14.1 h1:ySVCITvYsvBSiChOmr6GolLUcWX2T/ugykc2rjIaaQg= +github.com/go-telegram/bot v1.14.1/go.mod h1:i2TRs7fXWIeaceF3z7KzsMt/he0TwkVC680mvdTFYeM= +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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad h1:qIQkSlF5vAUHxEmTbaqt1hkJ/t6skqEGYiMag343ucI= +github.com/sasha-s/go-csync v0.0.0-20240107134140-fcbab37b09ad/go.mod h1:/pA7k3zsXKdjjAiUhB5CjuKib9KJGCaLvZwtxGC8U0s= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/main.go b/main.go new file mode 100644 index 0000000..17df0dd --- /dev/null +++ b/main.go @@ -0,0 +1,65 @@ +package main + +import ( + "context" + log "github.com/sirupsen/logrus" + "os" + "os/signal" + "syscall" + "tg-disc-bot/config" + "tg-disc-bot/discordBot" + "tg-disc-bot/dto" + "tg-disc-bot/tgBot" +) + +func main() { + c := config.NewConfig() + config.LogSetup(c.AppConf.LogLvl) + + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt) + defer cancel() + + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM) + + tgb, err := tgBot.NewTgBot(ctx, c.TgConf) + if err != nil { + log.Fatal(err) + } + + dsb, err := discordBot.NewDiscordBot(ctx, c.DsConf) + if err != nil { + log.Fatal(err) + } + + fromDiscord := make(chan dto.DiscordDTO, 100) + fromTelegram := make(chan dto.TelegramDTO, 100) + + tgMsgs := tgb.Start(fromDiscord) + dsMsgs := dsb.Start(fromTelegram) + + log.Info("App is now running. Press CTRL-C to exit.") + + for { + select { + case sig := <-shutdown: + { + log.WithField("type", sig).Info("terminating, close app") + os.Exit(0) + } + case <-ctx.Done(): + { + log.Info("terminating, close app") + os.Exit(0) + } + case tgMsg := <-tgMsgs: + { + fromTelegram <- tgMsg + } + case dsMsg := <-dsMsgs: + { + fromDiscord <- dsMsg + } + } + } +} diff --git a/tgBot/handler.go b/tgBot/handler.go new file mode 100644 index 0000000..ce963cf --- /dev/null +++ b/tgBot/handler.go @@ -0,0 +1,70 @@ +package tgBot + +import ( + "context" + "fmt" + "github.com/go-telegram/bot" + "github.com/go-telegram/bot/models" + log "github.com/sirupsen/logrus" + "tg-disc-bot/config" + "tg-disc-bot/dto" +) + +type TgBot struct { + ctx context.Context + bot *bot.Bot + chatID int64 +} + +func NewTgBot(ctx context.Context, config config.TelegramConfig) (*TgBot, error) { + tgBot := &TgBot{ctx: ctx, chatID: config.ChatID} + var err error + + tgBot.bot, err = bot.New(config.Token) + if err != nil { + return nil, err + } + + return tgBot, nil +} + +func (b *TgBot) Start(fromDiscord chan dto.DiscordDTO) chan dto.TelegramDTO { + + msgChan := make(chan dto.TelegramDTO, 100) + + b.bot.RegisterHandler( + bot.HandlerTypeMessageText, + "", + bot.MatchTypeContains, + func(ctx context.Context, bt *bot.Bot, update *models.Update) { + if update.Message != nil || !update.Message.From.IsBot { + msg := dto.TelegramDTO{ + AuthorName: update.Message.From.Username, + Content: update.Message.Text, + } + + fmt.Println(update.Message.Chat.ID) + + msgChan <- msg + } + }) + + go func() { + log.Info("Starting telegram bot...") + b.bot.Start(b.ctx) + }() + + go func() { + for msg := range fromDiscord { + log.WithField("content", msg).Debug("TG | Message from Discord") + m := fmt.Sprintf("[%s]\n%s", msg.AuthorName, msg.Content) + + b.bot.SendMessage(b.ctx, &bot.SendMessageParams{ + ChatID: b.chatID, + Text: m, + }) + } + }() + + return msgChan +}