Feat(Initial): Initial Go codebase
All checks were successful
Webhook-Everything/Webhook-Everything/pipeline/head This commit looks good

This commit is contained in:
2022-05-29 00:06:52 +08:00
parent f31bc0cd52
commit 53829a2788
27 changed files with 1489 additions and 0 deletions

View File

@@ -0,0 +1,81 @@
package common
import (
"fmt"
"log"
"os"
"time"
"github.com/DATA-DOG/go-sqlmock"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
type Database struct {
*gorm.DB
}
var DB *gorm.DB
// Opening a database and save the reference to `Database` struct.
func InitDB() *gorm.DB {
host := os.Getenv("DB_HOST")
user := os.Getenv("DB_USER")
pass := os.Getenv("DB_PASS")
dbName := os.Getenv("DB_NAME")
port := os.Getenv("DB_PORT")
var sslMode string
if os.Getenv("DB_SSL") == "TRUE" {
sslMode = "enable"
} else {
sslMode = "disable"
}
// DB Logger config
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Silent, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: true, // Disable color
},
)
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=%s TimeZone=Asia/Singapore", host, user, pass, dbName, port, sslMode)
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: newLogger,
})
if err != nil {
fmt.Println("db err: (Init) ", err)
}
DB = db
return DB
}
// This function will create a temporarily database for running testing cases
func TestDBInit() *gorm.DB {
testSQLDB, mock, err := sqlmock.New()
if err != nil {
panic(err)
}
testDB, err := gorm.Open(postgres.New(postgres.Config{
Conn: testSQLDB,
}), &gorm.Config{})
if err != nil {
fmt.Println("db err: (TestDBInit) ", err)
}
DB = testDB
_ = mock
return DB
}
// Using this function to get a connection, you can create your connection pool here.
func GetDB() *gorm.DB {
return DB
}

View File

@@ -0,0 +1,56 @@
package common
import (
"net/http"
"github.com/go-chi/render"
)
type ErrResponse struct {
Err error `json:"-"` // low-level runtime error
HTTPStatusCode int `json:"-"` // http response status code
StatusText string `json:"status"` // user-level status message
AppCode int64 `json:"code,omitempty"` // application-specific error code
ErrorText string `json:"error,omitempty"` // application-level error message, for debugging
}
func (e *ErrResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, e.HTTPStatusCode)
return nil
}
func ErrInvalidRequest(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 400,
StatusText: "Invalid request.",
ErrorText: err.Error(),
}
}
func ErrValidationError(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 422,
StatusText: "Validation error.",
ErrorText: err.Error(),
}
}
func ErrInternalError(err error) render.Renderer {
return &ErrResponse{
Err: err,
HTTPStatusCode: 500,
StatusText: "Invalid request.",
ErrorText: err.Error(),
}
}
func ErrNotFound(err error) render.Renderer {
return &ErrResponse{
HTTPStatusCode: 404,
StatusText: "Resource not found.",
ErrorText: err.Error(),
}
}

View File

@@ -0,0 +1,24 @@
package common
import (
"net/http"
"github.com/go-chi/render"
)
type TextResponse struct {
Status string `json:"status"` // user-level status message
Text string `json:"text"` // application-specific error code
}
func (e *TextResponse) Render(w http.ResponseWriter, r *http.Request) error {
render.Status(r, http.StatusOK)
return nil
}
func NewGenericTextResponse(status string, text string) render.Renderer {
return &TextResponse{
Status: status,
Text: text,
}
}

View File

@@ -0,0 +1,52 @@
package telegrampackage
import (
"fmt"
"math/rand"
"strings"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"gorm.io/gorm"
)
type TelegramHandlerFunction func(shortCode string, text string) (handled bool, responseText *string)
type Env struct {
DB *gorm.DB
TelegramAPIKey string
HandlerFunctions []TelegramHandlerFunction
}
func NewEnv(db *gorm.DB, telegramAPIKey string, handlerFunctions []TelegramHandlerFunction) *Env {
var env Env
env.DB = db
env.TelegramAPIKey = telegramAPIKey
env.HandlerFunctions = handlerFunctions
// Example handler function
_ = env.telegramHandlerExample
// Seed random
rand.Seed(time.Now().UnixNano())
// Start listening job
go env.TelegramListenAndGen()
return &env
}
func (env *Env) AddTelegramHandlerFunc(handlerFunction TelegramHandlerFunction) {
env.HandlerFunctions = append(env.HandlerFunctions, handlerFunction)
}
// Not used normally. Shown here as example only
func (env *Env) telegramHandlerExample(shortCode string, text string) (bool, *string) {
commandSplitted := ParseTelegramBotCommand(text)
if len(commandSplitted) > 0 && commandSplitted[0] == "/command" {
responseText := fmt.Sprintf("Your short code is %s.\n\nYour message is %s", shortCode, text)
responseText = responseText + fmt.Sprintf("\n\nYour parameters are: %s", strings.Join(commandSplitted[1:], "\n - "))
responseText = "<code>" + tgbotapi.EscapeText("HTML", responseText) + "</code>"
return true, &responseText
}
return false, nil
}

View File

@@ -0,0 +1,35 @@
package telegrampackage
import "strings"
func ParseTelegramBotCommand(fullCmd string) []string {
var results []string
currentInsideQuote := false
splitted := strings.Split(fullCmd, " ")
for _, currSplit := range splitted {
if !currentInsideQuote {
if strings.HasPrefix(currSplit, "\"") {
if len(currSplit) >= 2 && !strings.HasSuffix(currSplit, "\\\"") && strings.HasSuffix(currSplit, "\"") {
currSplit = strings.ReplaceAll(currSplit, "\\\"", "\"")
results = append(results, currSplit[1:len(currSplit)-1])
} else {
currentInsideQuote = true
currSplit = strings.ReplaceAll(currSplit, "\\\"", "\"")
results = append(results, currSplit[1:])
}
} else {
results = append(results, currSplit)
}
} else {
if !strings.HasSuffix(currSplit, "\\\"") && strings.HasSuffix(currSplit, "\"") {
currentInsideQuote = false
currSplit = strings.ReplaceAll(currSplit, "\\\"", "\"")
results[len(results)-1] = results[len(results)-1] + " " + currSplit[:len(currSplit)-1]
} else {
currSplit = strings.ReplaceAll(currSplit, "\\\"", "\"")
results[len(results)-1] = results[len(results)-1] + " " + currSplit
}
}
}
return results
}

View File

@@ -0,0 +1,173 @@
package telegrampackage
import (
"errors"
"fmt"
"log"
"math/rand"
"strings"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
"github.com/google/uuid"
"gorm.io/gorm"
)
type ChatIDMap struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
ChatID int64
ChatIDShort string
}
func (env *Env) TelegramSend(chatIDShort string, msg string) error {
if chatIDShort == "" {
return errors.New("no chat ID provided")
}
bot, err := tgbotapi.NewBotAPI(env.TelegramAPIKey)
if err != nil {
log.Println(err)
return err
}
var chatIDMap ChatIDMap
err = env.DB.Where(&ChatIDMap{ChatIDShort: chatIDShort}).Last(&chatIDMap).Error
if err != nil {
log.Println(err)
log.Println("No such telegram chat ID")
return err
}
chatMsg := tgbotapi.NewMessage(chatIDMap.ChatID, msg)
_, err = bot.Send(chatMsg)
if err != nil {
log.Println(err)
}
return nil
}
func (env *Env) TelegramListenAndGen() error {
for {
bot, err := tgbotapi.NewBotAPI(env.TelegramAPIKey)
if err != nil {
log.Println(err)
log.Println("Error occured on Telegram listen. Waiting 1 minute")
time.Sleep(60 * time.Second)
}
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil { // ignore any non-Message Updates
continue
}
log.Printf("New message: [%s] %s", update.Message.From.UserName, update.Message.Text)
// check if contains registration
if strings.Compare(update.Message.Text, "/register") == 0 {
var chatIDMap ChatIDMap
err := env.DB.Where(&ChatIDMap{ChatID: update.Message.Chat.ID}).Last(&chatIDMap).Error
// chatID already exists
if err == nil {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "You already have a chat ID")
_, err := bot.Send(msg)
if err != nil {
log.Println(err)
}
msg = tgbotapi.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Your chat ID is: %s", chatIDMap.ChatIDShort))
_, err = bot.Send(msg)
if err != nil {
log.Println(err)
}
} else {
currChatCode := genShortCode(5)
chatIDMap.ChatIDShort = currChatCode
chatIDMap.ChatID = update.Message.Chat.ID
err := env.DB.Create(&chatIDMap).Error
if err != nil {
fmt.Println(err)
}
msg := tgbotapi.NewMessage(update.Message.Chat.ID, fmt.Sprintf("Your chat ID is: %s", chatIDMap.ChatIDShort))
_, err = bot.Send(msg)
if err != nil {
log.Println(err)
}
}
} else {
// Get his current chat ID
var chatIDMap ChatIDMap
err := env.DB.Where(&ChatIDMap{ChatID: update.Message.Chat.ID}).Last(&chatIDMap).Error
if err != nil {
fmt.Println(err)
}
var handled = false
for _, currFunc := range env.HandlerFunctions {
currHandled, responseText := currFunc(chatIDMap.ChatIDShort, update.Message.Text)
handled = handled || currHandled
if responseText != nil {
splitSendMessage(bot, update.Message.Chat.ID, *responseText)
}
}
if !handled {
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "Please send /register to register for a chat ID")
_, err := bot.Send(msg)
if err != nil {
log.Println(err)
}
}
}
}
}
}
func genShortCode(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ")
runeCode := make([]rune, n)
for i := range runeCode {
runeCode[i] = letters[rand.Intn(len(letters))]
}
return string(runeCode)
}
func splitSendMessage(bot *tgbotapi.BotAPI, chatID int64, message string) {
MAXMESSAGELENGTH := 2000
leftoverMessage := []rune(tgbotapi.EscapeText("HTML", message))
if len(leftoverMessage) > MAXMESSAGELENGTH {
for len(leftoverMessage) > 0 {
var currMessage []rune
if len(leftoverMessage) <= MAXMESSAGELENGTH {
currMessage = leftoverMessage[:]
leftoverMessage = []rune("")
} else {
currMessage = leftoverMessage[:MAXMESSAGELENGTH]
leftoverMessage = leftoverMessage[MAXMESSAGELENGTH:]
}
msg := tgbotapi.NewMessage(chatID, string(currMessage))
msg.ParseMode = "HTML"
msg.DisableWebPagePreview = true
_, err := bot.Send(msg)
if err != nil {
msg := tgbotapi.NewMessage(chatID, err.Error())
bot.Send(msg)
log.Println(err)
}
}
} else {
msg := tgbotapi.NewMessage(chatID, string(message))
msg.ParseMode = "HTML"
msg.DisableWebPagePreview = true
_, err := bot.Send(msg)
if err != nil {
msg := tgbotapi.NewMessage(chatID, err.Error())
bot.Send(msg)
log.Println(err)
}
}
}

View File

@@ -0,0 +1,34 @@
package webhookeverything
import (
"math/rand"
"time"
"git.samuelpua.com/telboon/webhook-everything/backend/internal/telegrampackage"
"github.com/go-chi/chi"
"gorm.io/gorm"
)
type Env struct {
DB *gorm.DB
TelegramEnv *telegrampackage.Env
HostUrl string
}
func WebhookEverythingRoutes(db *gorm.DB, telegramEnv *telegrampackage.Env, hostURL string) chi.Router {
var env Env
env.DB = db
env.TelegramEnv = telegramEnv
// Seed random
rand.Seed(time.Now().UnixNano())
// Web Routes
r := chi.NewRouter()
r.HandleFunc("/routes/{routeID}", env.handleWebhook)
// Telegram handlers
env.TelegramEnv.AddTelegramHandlerFunc(env.registerWebhook)
return r
}

View File

@@ -0,0 +1,40 @@
package webhookeverything
import (
"fmt"
"math/rand"
"net/url"
"path"
"git.samuelpua.com/telboon/webhook-everything/backend/internal/telegrampackage"
)
func (env *Env) registerWebhook(shortCode string, text string) (bool, *string) {
commandSplitted := telegrampackage.ParseTelegramBotCommand(text)
if len(commandSplitted) > 0 && commandSplitted[0] == "/register-webhook" {
newWebhookID := genWebhookCode(6)
baseURL, _ := url.Parse(env.HostUrl)
baseURL.Path = path.Join(baseURL.Path, "webhook")
baseURL.Path = path.Join(baseURL.Path, "routes")
baseURL.Path = path.Join(baseURL.Path, newWebhookID)
webhookURL := baseURL.String()
var webhookRoute WebhookRoute
webhookRoute.TelegramShortCode = shortCode
webhookRoute.WebhookID = newWebhookID
env.DB.Create(&webhookRoute)
responseText := fmt.Sprintf("Your generated webhook URL is: %s", webhookURL)
return true, &responseText
}
return false, nil
}
func genWebhookCode(n int) string {
var letters = []rune("abcdefghijklmnopqrstuvwxyz1234567890")
runeCode := make([]rune, n)
for i := range runeCode {
runeCode[i] = letters[rand.Intn(len(letters))]
}
return string(runeCode)
}

View File

@@ -0,0 +1,26 @@
package webhookeverything
import (
"net/http"
"net/http/httputil"
)
func (env *Env) forwardHookToTelegram(r *http.Request, routeID string) error {
// Get Telegram code
var routeResult WebhookRoute
err := env.DB.Where(&WebhookRoute{WebhookID: routeID}).First(&routeResult).Error
if err != nil {
return err
}
// Dump request as string
responseStr, err := httputil.DumpRequest(r, true)
if err != nil {
return err
}
// Send telegram
env.TelegramEnv.TelegramSend(routeResult.TelegramShortCode, string(responseStr))
return nil
}

View File

@@ -0,0 +1,28 @@
package webhookeverything
import (
"net/http"
"time"
"github.com/google/uuid"
"gorm.io/gorm"
)
type WebhookRoute struct {
ID uuid.UUID `gorm:"primaryKey;type:uuid;default:uuid_generate_v4()"`
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt gorm.DeletedAt `gorm:"index"`
WebhookID string `gorm:"index,unique"`
TelegramShortCode string `gorm:"index"`
}
type StatusMessage struct {
Status string `json:"status"`
Message string `json:"message"`
}
func (statusMessage *StatusMessage) Render(w http.ResponseWriter, r *http.Request) error {
// Pre-processing before a response is marshalled and sent across the wire
return nil
}

View File

@@ -0,0 +1,34 @@
package webhookeverything
import (
"net/http"
"git.samuelpua.com/telboon/webhook-everything/backend/internal/common"
"github.com/go-chi/chi"
"github.com/go-chi/render"
)
// Handles pre-generated webhooks
// @Summary Pre-generated webhooks
// @Description Description
// @Tags Webhook
// @Accept json
// @Produce json
// @Param webhookID path string true "Pre-registered Webhook Path"
// @Success 200 {object} common.TextResponse
// @Failure 500 {object} common.ErrResponse
// @Router /webhook/routes/{webhookID} [POST]
func (env *Env) handleWebhook(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
routeID := chi.URLParam(r, "routeID")
_ = ctx
err := env.forwardHookToTelegram(r, routeID)
if err != nil {
render.Render(w, r, common.ErrInternalError(err))
return
}
render.Render(w, r, common.NewGenericTextResponse("success", ""))
}