From 05558cdf808e2979d649e8bed3e13b7fa9c62f1c Mon Sep 17 00:00:00 2001
From: RandomChars <random@chars.jp>
Date: Sun, 7 Nov 2021 01:44:50 +0900
Subject: [PATCH] sessions, config, basic handling of telegram side of things

---
 .gitignore         |   5 ++
 cleanup.go         |  15 ++++
 config.go          | 157 +++++++++++++++++++++++++++++++++
 discord.go         |  53 +++++++++++
 go.mod             |  12 +++
 go.sum             |  22 +++++
 main.go            |  53 +++++++++++
 restart.go         |  26 ++++++
 restart_windows.go |  30 +++++++
 telegram.go        | 213 +++++++++++++++++++++++++++++++++++++++++++++
 10 files changed, 586 insertions(+)
 create mode 100644 .gitignore
 create mode 100644 cleanup.go
 create mode 100644 config.go
 create mode 100644 discord.go
 create mode 100644 go.sum
 create mode 100644 main.go
 create mode 100644 restart.go
 create mode 100644 restart_windows.go
 create mode 100644 telegram.go

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6686796
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,5 @@
+bridgething
+bridge.conf
+
+*.swp
+.idea/
\ No newline at end of file
diff --git a/cleanup.go b/cleanup.go
new file mode 100644
index 0000000..2ebb497
--- /dev/null
+++ b/cleanup.go
@@ -0,0 +1,15 @@
+package main
+
+import "log"
+
+func cleanup() {
+	if botAPI != nil {
+		botAPI.StopReceivingUpdates()
+	}
+
+	if session != nil {
+		if err := session.Close(); err != nil {
+			log.Printf("error closing discord session: %s", err)
+		}
+	}
+}
diff --git a/config.go b/config.go
new file mode 100644
index 0000000..e252e84
--- /dev/null
+++ b/config.go
@@ -0,0 +1,157 @@
+package main
+
+import (
+	"flag"
+	"github.com/pelletier/go-toml/v2"
+	"log"
+	"os"
+	"strconv"
+)
+
+var (
+	config   conf
+	confPath string
+	parsed   = false
+
+	discordBridge  = make(map[string]bridgeConf)
+	telegramBridge = make(map[int64]bridgeConf)
+)
+
+func init() {
+	flag.StringVar(&confPath, "c", "bridge.conf", "specify location of configuration file")
+}
+
+type conf struct {
+	System   systemConf
+	Telegram telegramConf
+	Discord  discordConf
+	Bridges  struct{ Item []bridgeConf }
+}
+
+type systemConf struct {
+	Verbose        bool
+	DisplaceHeader bool
+}
+
+type telegramConf struct {
+	Token         string
+	NameFormat    string
+	StickerEmoji  bool
+	BypassBacklog bool
+}
+
+type discordConf struct {
+	Token       string
+	NameFormat  string
+	StickerText bool
+}
+
+type bridgeConf struct {
+	Label    string
+	Discord  bridgePlatformConf
+	Telegram bridgePlatformConf
+}
+
+type bridgePlatformConf struct {
+	ID     int
+	Enable bool
+	Join   bool
+	Leave  bool
+	Delete bool
+}
+
+func setupConfig() {
+	if parsed {
+		panic("config already parsed")
+	}
+	defer func() { parsed = true }()
+
+	if file, err := os.Open(confPath); err != nil {
+		if !os.IsNotExist(err) {
+			log.Fatalf("error opening configuration file: %s", err)
+		}
+		if file, err = os.Create(confPath); err != nil {
+			log.Fatalf("error creating configuration file: %s", err)
+		} else {
+			encoder := toml.NewEncoder(file)
+			encoder.SetIndentSymbol("    ")
+			encoder.SetIndentTables(true)
+
+			if err = encoder.Encode(defConf); err != nil {
+				log.Fatalf("error generating default configuration: %s", err)
+			} else {
+				log.Print("default configuration generated")
+				os.Exit(1)
+			}
+		}
+	} else if err = toml.NewDecoder(file).Decode(&config); err != nil {
+		log.Fatalf("error parsing configuration: %s", err)
+	} else {
+		if err = file.Close(); err != nil {
+			log.Fatalf("error closing configuration: %s", err)
+		}
+	}
+
+	for _, c := range config.Bridges.Item {
+		if c.Discord.Enable {
+			discordBridge[strconv.Itoa(c.Discord.ID)] = c
+		}
+		if c.Telegram.Enable {
+			telegramBridge[int64(c.Telegram.ID)] = c
+		}
+	}
+}
+
+var defConf = conf{
+	System: systemConf{
+		Verbose:        false,
+		DisplaceHeader: true,
+	},
+	Telegram: telegramConf{
+		Token:         "INSERT_TOKEN_HERE",
+		NameFormat:    "**$FIRST$SPACE$LAST (@$USER)**",
+		StickerEmoji:  true,
+		BypassBacklog: true,
+	},
+	Discord: discordConf{
+		Token:       "INSERT_TOKEN_HERE",
+		NameFormat:  "$USER#$DISCRIMINATOR",
+		StickerText: true,
+	},
+	Bridges: struct{ Item []bridgeConf }{Item: []bridgeConf{
+		{
+			Label: "Default",
+			Discord: bridgePlatformConf{
+				ID:     0,
+				Enable: true,
+				Join:   true,
+				Leave:  true,
+				Delete: true,
+			},
+			Telegram: bridgePlatformConf{
+				ID:     0,
+				Enable: true,
+				Join:   true,
+				Leave:  true,
+				Delete: true,
+			},
+		},
+		{
+			Label: "Disabled",
+			Discord: bridgePlatformConf{
+				ID:     0,
+				Enable: false,
+				Join:   true,
+				Leave:  true,
+				Delete: true,
+			},
+			Telegram: bridgePlatformConf{
+				ID:     0,
+				Enable: false,
+				Join:   true,
+				Leave:  true,
+				Delete: true,
+			},
+		},
+	}},
+}
diff --git a/discord.go b/discord.go
new file mode 100644
index 0000000..db59670
--- /dev/null
+++ b/discord.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+	"github.com/bwmarrin/discordgo"
+	"log"
+)
+
+var session *discordgo.Session
+
+func init() {
+	discordgo.Logger = func(msgL, _ int, format string, a ...interface{}) {
+		prefix := "discord: "
+		switch msgL {
+		case discordgo.LogError:
+			prefix += "[error] "
+		case discordgo.LogWarning:
+			prefix += "[warning] "
+		case discordgo.LogInformational:
+			prefix += "[informational] "
+		case discordgo.LogDebug:
+			if !config.System.Verbose {
+				return
+			}
+			prefix += "[debug] "
+		}
+		log.Printf(prefix+format, a...)
+	}
+}
+
+func openDiscord() {
+	if s, err := discordgo.New(); err != nil {
+		// this should never happen
+		log.Fatalf("error creating discord session: %s", err)
+	} else {
+		session = s
+	}
+
+	session.Identify.Intents = discordgo.IntentsGuildMessages
+	session.Token = "Bot " + config.Discord.Token
+
+	if err := session.Open(); err != nil {
+		log.Fatalf("error connecting to discord: %s", err)
+	} else {
+		log.Printf("connected to discord as %s#%s (%s)",
+			session.State.User.Username, session.State.User.Discriminator, session.State.User.ID)
+	}
+
+	ready <- struct{}{}
+}
+
+func handleDiscord() {
+	// TODO
+}
diff --git a/go.mod b/go.mod
index 5945258..954d435 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,15 @@
 module random.chars.jp/git/bridgething
 
 go 1.17
+
+require (
+	github.com/bwmarrin/discordgo v0.23.2
+	github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
+	github.com/pelletier/go-toml/v2 v2.0.0-beta.3
+)
+
+require (
+	github.com/gorilla/websocket v1.4.0 // indirect
+	github.com/technoweenie/multipartstreamer v1.0.1 // indirect
+	golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..b92f49b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,22 @@
+github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
+github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible h1:2cauKuaELYAEARXRkq2LrJ0yDDv1rW7+wrTEdVL3uaU=
+github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible/go.mod h1:qf9acutJ8cwBUhm1bqgz6Bei9/C/c93FPDljKWwsOgM=
+github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
+github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
+github.com/pelletier/go-toml/v2 v2.0.0-beta.3 h1:PNCTU4naEJ8mKal97P3A2qDU74QRQGlv4FXiL1XDqi4=
+github.com/pelletier/go-toml/v2 v2.0.0-beta.3/go.mod h1:aNseLYu/uKskg0zpr/kbr2z8yGuWtotWf/0BpGIAL2Y=
+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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942 h1:t0lM6y/M5IiUZyvbBTcngso8SZEZICH7is9B6g/obVU=
+github.com/stretchr/testify v1.7.1-0.20210427113832-6241f9ab9942/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
+github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
+golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..415593f
--- /dev/null
+++ b/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+	"flag"
+	"log"
+	"os"
+	"os/signal"
+	"syscall"
+)
+
+var (
+	exec  string
+	rest  bool
+	ready = make(chan struct{})
+)
+
+func main() {
+	flag.Parse()
+
+	setupConfig()
+	go openTelegram()
+	go openDiscord()
+
+	go func() {
+		<-ready
+		<-ready
+		log.Print("sessions ready, starting handlers")
+		go handleDiscord()
+		go handleTelegram()
+	}()
+
+	s := make(chan os.Signal, 1)
+	signal.Notify(s, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
+	func() {
+		defer func() { cleanup() }()
+		for {
+			sig := <-s
+			switch sig {
+			case syscall.SIGINT:
+				println()
+				log.Print("shutting down")
+				return
+			default:
+				log.Print("shutting down")
+				return
+			}
+		}
+	}()
+
+	if rest {
+		restart()
+	}
+}
diff --git a/restart.go b/restart.go
new file mode 100644
index 0000000..7442e19
--- /dev/null
+++ b/restart.go
@@ -0,0 +1,26 @@
+//go:build !windows
+
+package main
+
+import (
+	"log"
+	"os"
+	"syscall"
+)
+
+func restart() {
+	var err error
+	if exec == "" {
+		return
+	}
+
+	if _, err = os.Stat(exec); err != nil {
+		log.Fatalf("error stat exec: %s", err)
+	}
+
+	log.Printf("execve %s", exec)
+	err = syscall.Exec(exec, os.Args, os.Environ())
+	if err != nil {
+		log.Fatalf("error execve: %s", err)
+	}
+}
diff --git a/restart_windows.go b/restart_windows.go
new file mode 100644
index 0000000..fa2e49c
--- /dev/null
+++ b/restart_windows.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+	"log"
+	"os"
+)
+
+func restart() {
+	if exec == "" {
+		return
+	}
+
+	if _, err := os.Stat(executable); err != nil {
+		log.Fatalf("error stat exec: %s", err)
+	}
+	wd, err := os.Getwd()
+	if err != nil {
+		log.Fatalf("error getwd: %s", err)
+	}
+	log.Printf("pwd is %s", wd)
+
+	if _, err = os.StartProcess(executable, []string{}, &os.ProcAttr{
+		Dir:   wd,
+		Env:   nil,
+		Files: []*os.File{os.Stderr, os.Stdin, os.Stdout},
+		Sys:   nil,
+	}); err != nil {
+		log.Fatalf("error start process: %s", err)
+	}
+}
diff --git a/telegram.go b/telegram.go
new file mode 100644
index 0000000..3e8ac1b
--- /dev/null
+++ b/telegram.go
@@ -0,0 +1,213 @@
+package main
+
+import (
+	"github.com/bwmarrin/discordgo"
+	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
+	"io"
+	"log"
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+)
+
+var botAPI *tgbotapi.BotAPI
+
+type botLogger struct{}
+
+func (botLogger) Printf(format string, v ...interface{}) {
+	log.Printf("telegram: "+format, v...)
+}
+func (botLogger) Println(v ...interface{}) {
+	log.Println(append([]interface{}{"telegram:"}, v...)...)
+}
+
+func init() {
+	if err := tgbotapi.SetLogger(botLogger{}); err != nil {
+		panic(err)
+	}
+}
+
+func openTelegram() {
+	if bot, err := tgbotapi.NewBotAPI(config.Telegram.Token); err != nil {
+		log.Fatalf("error connecting to telegram: %s", err)
+	} else {
+		botAPI = bot
+	}
+	log.Printf("connected to telegram as @%s (%v)", botAPI.Self.UserName, botAPI.Self.ID)
+	botAPI.Debug = config.System.Verbose
+
+	ready <- struct{}{}
+}
+
+func handleTelegram() {
+	u := tgbotapi.NewUpdate(0)
+	u.Timeout = 60
+
+	if updates, err := botAPI.GetUpdatesChan(u); err != nil {
+		log.Fatalf("error getting updates: %s", err)
+	} else {
+		if config.Telegram.BypassBacklog {
+			time.Sleep(time.Millisecond * 500)
+			updates.Clear()
+		}
+
+		for update := range updates {
+			respondTelegram(update)
+		}
+	}
+}
+
+var previousCaller = make(map[int64]int)
+
+func setPreviousCaller(cid int64, uid int) bool {
+	if previousCaller[cid] == uid {
+		return true
+	}
+	previousCaller[cid] = uid
+	return false
+}
+
+func respondTelegram(update tgbotapi.Update) {
+	// TODO: cross reply
+	// TODO: sticker object storage
+
+	if update.Message == nil || update.Message.Chat == nil {
+		if update.EditedMessage != nil {
+			// TODO: handle edit
+			return
+		}
+		if config.System.Verbose {
+			log.Printf("got irrelevant update %v", update.UpdateID)
+		}
+		return
+	}
+
+	if update.Message.IsCommand() {
+		message := tgbotapi.NewMessage(update.Message.Chat.ID, "")
+		switch update.Message.Command() {
+		case "id":
+			message.Text = strconv.Itoa(int(update.Message.Chat.ID))
+		default:
+		}
+		if message.Text != "" {
+			message.ReplyToMessageID = update.Message.MessageID
+			if _, err := botAPI.Send(message); err != nil {
+				log.Printf("error responding to command %v, %s", update.Message.Chat.ID, err)
+			}
+			return
+		}
+	}
+
+	var (
+		dc bridgePlatformConf
+		tc bridgePlatformConf
+	)
+	if c, ok := telegramBridge[update.Message.Chat.ID]; !ok {
+		return
+	} else {
+		dc = c.Discord
+		tc = c.Telegram
+	}
+
+	var (
+		username = "unknown"
+		id       = -1
+		dMsgID   = "-1"
+	)
+	if update.Message.From != nil {
+		username = update.Message.From.UserName
+		id = update.Message.From.ID
+	}
+
+	space := ""
+	if update.Message.From.LastName != "" {
+		space = " "
+	}
+	name := ""
+	if !config.System.DisplaceHeader || !setPreviousCaller(update.Message.Chat.ID, update.Message.From.ID) {
+		// TODO: check the discord side for header displace
+		name = strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(strings.ReplaceAll(config.Telegram.NameFormat,
+			"$FIRST", update.Message.From.FirstName),
+			"$LAST", update.Message.From.LastName),
+			"$USER", update.Message.From.UserName),
+			"$SPACE", space)
+		name += "\n"
+	}
+
+	if update.Message.Sticker != nil {
+		var url string
+		if u, err := botAPI.GetFileDirectURL(update.Message.Sticker.FileID); err != nil {
+			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "error getting URL")
+			msg.ReplyToMessageID = update.Message.MessageID
+			log.Printf("error getting URL of %s: %s", update.Message.Sticker.FileID, err)
+			_, _ = botAPI.Send(msg)
+			return
+		} else {
+			url = u
+		}
+
+		var (
+			file *discordgo.File
+			body io.ReadCloser
+		)
+		if resp, err := http.Get(url); err != nil {
+			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "error getting file")
+			msg.ReplyToMessageID = update.Message.MessageID
+			log.Printf("error getting file %s: %s", update.Message.Sticker.FileID, err)
+			_, _ = botAPI.Send(msg)
+			return
+		} else {
+			file = &discordgo.File{
+				Name:        "sticker.webp",
+				ContentType: "image/webp",
+				Reader:      resp.Body,
+			}
+			body = resp.Body
+		}
+
+		content := name
+		if config.Telegram.StickerEmoji {
+			content += update.Message.Sticker.Emoji
+		}
+		if message, err := session.ChannelMessageSendComplex(strconv.Itoa(dc.ID), &discordgo.MessageSend{
+			Content: content,
+			File:    file,
+		}); err != nil {
+			msg := tgbotapi.NewMessage(update.Message.Chat.ID, "error relaying message")
+			msg.ReplyToMessageID = update.Message.MessageID
+			log.Printf("error relaying message %v: %s", update.Message.MessageID, err)
+			_, _ = botAPI.Send(msg)
+			return
+		} else {
+			dMsgID = message.ID
+		}
+		if err := body.Close(); err != nil {
+			log.Printf("error closing request body: %s", err)
+		}
+		log.Printf("T%vM%v -> D%vM%s @%s (%v): %s",
+			tc.ID, update.Message.MessageID, dc.ID, dMsgID, username, id, "Sticker: "+update.Message.Sticker.FileID)
+		return
+	}
+
+	if update.Message.Contact != nil {
+		// TODO: handle contact
+		return
+	}
+
+	if update.Message.Text == "" {
+		return
+	}
+
+	if message, err := session.ChannelMessageSend(strconv.Itoa(dc.ID), name+update.Message.Text); err != nil {
+		msg := tgbotapi.NewMessage(update.Message.Chat.ID, "error relaying message")
+		msg.ReplyToMessageID = update.Message.MessageID
+		log.Printf("error relaying message %v: %s", update.Message.MessageID, err.Error())
+		_, _ = botAPI.Send(msg)
+	} else {
+		dMsgID = message.ID
+	}
+
+	log.Printf("T%vM%v -> D%vM%s @%s (%v): %s",
+		tc.ID, update.Message.MessageID, dc.ID, dMsgID, username, id, update.Message.Text)
+}
-- 
GitLab