Browse Source

reorganize code

master
Nicolas Massé 6 years ago
parent
commit
21499a592b
  1. 145
      bot.go
  2. 33
      http.go
  3. 109
      main.go
  4. 17
      security.go
  5. 81
      web.go

145
bot.go

@ -13,13 +13,8 @@ import (
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api" tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
) )
type PhotoBot struct { type TelegramBot struct {
Telegram TelegramBackend
MediaStore *MediaStore MediaStore *MediaStore
WebInterface WebInterface
}
type TelegramBackend struct {
TokenGenerator *TokenGenerator TokenGenerator *TokenGenerator
GlobalTokenValidity int GlobalTokenValidity int
PerAlbumTokenValidity int PerAlbumTokenValidity int
@ -57,42 +52,40 @@ type TelegramMessages struct {
SharedGlobal string SharedGlobal string
} }
func InitBot(targetDir string) *PhotoBot { func NewTelegramBot() *TelegramBot {
return &PhotoBot{ bot := TelegramBot{}
Telegram: TelegramBackend{ bot.AuthorizedUsers = make(map[string]bool)
AuthorizedUsers: make(map[string]bool), return &bot
RetryDelay: time.Duration(30) * time.Second,
},
}
} }
func (bot *PhotoBot) StartBot(token string) { func (bot *TelegramBot) StartBot(token string, debug bool) {
var telegramBot *tgbotapi.BotAPI var telegramBot *tgbotapi.BotAPI
var err error var err error
for tryAgain := true; tryAgain; tryAgain = (err != nil) { for tryAgain := true; tryAgain; tryAgain = (err != nil) {
telegramBot, err = tgbotapi.NewBotAPI(token) telegramBot, err = tgbotapi.NewBotAPI(token)
if err != nil { if err != nil {
log.Printf("Cannot start the Telegram Bot because of '%s'. Retrying in %d seconds...", err, bot.Telegram.RetryDelay/time.Second) log.Printf("Cannot start the Telegram Bot because of '%s'. Retrying in %d seconds...", err, bot.RetryDelay/time.Second)
time.Sleep(bot.Telegram.RetryDelay) time.Sleep(bot.RetryDelay)
} }
} }
log.Printf("Authorized on account %s", telegramBot.Self.UserName) log.Printf("Authorized on account %s", telegramBot.Self.UserName)
bot.Telegram.API = telegramBot bot.API = telegramBot
bot.API.Debug = debug
} }
func (bot *PhotoBot) Process() { func (bot *TelegramBot) Process() {
u := tgbotapi.NewUpdate(0) u := tgbotapi.NewUpdate(0)
u.Timeout = bot.Telegram.NewUpdateTimeout u.Timeout = bot.NewUpdateTimeout
updates, _ := bot.Telegram.API.GetUpdatesChan(u) updates, _ := bot.API.GetUpdatesChan(u)
for update := range updates { for update := range updates {
bot.ProcessUpdate(update) bot.ProcessUpdate(update)
} }
} }
func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) { func (bot *TelegramBot) ProcessUpdate(update tgbotapi.Update) {
if update.Message == nil || update.Message.From == nil { if update.Message == nil || update.Message.From == nil {
return return
} }
@ -105,29 +98,29 @@ func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) {
username := update.Message.From.UserName username := update.Message.From.UserName
if username == "" { if username == "" {
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.NoUsername) bot.replyToCommandWithMessage(update.Message, bot.Messages.NoUsername)
return return
} }
if !bot.Telegram.AuthorizedUsers[username] { if !bot.AuthorizedUsers[username] {
log.Printf("[%s] unauthorized user", username) log.Printf("[%s] unauthorized user", username)
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.Forbidden) bot.replyToCommandWithMessage(update.Message, bot.Messages.Forbidden)
return return
} }
err := bot.Telegram.ChatDB.UpdateWith(username, update.Message.Chat.ID) err := bot.ChatDB.UpdateWith(username, update.Message.Chat.ID)
if err != nil { if err != nil {
log.Printf("[%s] cannot update chat db: %s", username, err) log.Printf("[%s] cannot update chat db: %s", username, err)
} }
if update.Message.ReplyToMessage != nil { if update.Message.ReplyToMessage != nil {
// Only deal with forced replies (reply to bot's messages) // Only deal with forced replies (reply to bot's messages)
if update.Message.ReplyToMessage.From == nil || update.Message.ReplyToMessage.From.UserName != bot.Telegram.API.Self.UserName { if update.Message.ReplyToMessage.From == nil || update.Message.ReplyToMessage.From.UserName != bot.API.Self.UserName {
return return
} }
if update.Message.ReplyToMessage.Text != "" { if update.Message.ReplyToMessage.Text != "" {
if update.Message.ReplyToMessage.Text == bot.Telegram.Messages.MissingAlbumName { if update.Message.ReplyToMessage.Text == bot.Messages.MissingAlbumName {
log.Printf("[%s] reply to previous command /%s: %s", username, bot.Telegram.Commands.NewAlbum, text) log.Printf("[%s] reply to previous command /%s: %s", username, bot.Commands.NewAlbum, text)
bot.handleNewAlbumCommandReply(update.Message) bot.handleNewAlbumCommandReply(update.Message)
return return
} }
@ -138,66 +131,66 @@ func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) {
if update.Message.IsCommand() { if update.Message.IsCommand() {
log.Printf("[%s] command: %s", username, text) log.Printf("[%s] command: %s", username, text)
switch update.Message.Command() { switch update.Message.Command() {
case "start", bot.Telegram.Commands.Help: case "start", bot.Commands.Help:
bot.handleHelpCommand(update.Message) bot.handleHelpCommand(update.Message)
case bot.Telegram.Commands.Share: case bot.Commands.Share:
bot.handleShareCommand(update.Message) bot.handleShareCommand(update.Message)
case bot.Telegram.Commands.Browse: case bot.Commands.Browse:
bot.handleBrowseCommand(update.Message) bot.handleBrowseCommand(update.Message)
case bot.Telegram.Commands.NewAlbum: case bot.Commands.NewAlbum:
bot.handleNewAlbumCommand(update.Message) bot.handleNewAlbumCommand(update.Message)
case bot.Telegram.Commands.Info: case bot.Commands.Info:
bot.handleInfoCommand(update.Message) bot.handleInfoCommand(update.Message)
default: default:
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.DoNotUnderstand) bot.replyToCommandWithMessage(update.Message, bot.Messages.DoNotUnderstand)
} }
} else { } else {
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.DoNotUnderstand) bot.replyToCommandWithMessage(update.Message, bot.Messages.DoNotUnderstand)
} }
} else if update.Message.Photo != nil { } else if update.Message.Photo != nil {
err := bot.handlePhoto(update.Message) err := bot.handlePhoto(update.Message)
if err != nil { if err != nil {
log.Printf("[%s] cannot add photo to current album: %s", username, err) log.Printf("[%s] cannot add photo to current album: %s", username, err)
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.ServerError) bot.replyToCommandWithMessage(update.Message, bot.Messages.ServerError)
return return
} }
bot.dispatchMessage(update.Message) bot.dispatchMessage(update.Message)
bot.Telegram.replyWithMessage(update.Message, bot.Telegram.Messages.ThankYouMedia) bot.replyWithMessage(update.Message, bot.Messages.ThankYouMedia)
} else if update.Message.Video != nil { } else if update.Message.Video != nil {
err := bot.handleVideo(update.Message) err := bot.handleVideo(update.Message)
if err != nil { if err != nil {
log.Printf("[%s] cannot add video to current album: %s", username, err) log.Printf("[%s] cannot add video to current album: %s", username, err)
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.ServerError) bot.replyToCommandWithMessage(update.Message, bot.Messages.ServerError)
return return
} }
bot.dispatchMessage(update.Message) bot.dispatchMessage(update.Message)
bot.Telegram.replyWithMessage(update.Message, bot.Telegram.Messages.ThankYouMedia) bot.replyWithMessage(update.Message, bot.Messages.ThankYouMedia)
} else { } else {
log.Printf("[%s] cannot handle this type of message", username) log.Printf("[%s] cannot handle this type of message", username)
bot.Telegram.replyToCommandWithMessage(update.Message, bot.Telegram.Messages.DoNotUnderstand) bot.replyToCommandWithMessage(update.Message, bot.Messages.DoNotUnderstand)
} }
} }
func (bot *PhotoBot) dispatchMessage(message *tgbotapi.Message) { func (bot *TelegramBot) dispatchMessage(message *tgbotapi.Message) {
for user, _ := range bot.Telegram.AuthorizedUsers { for user, _ := range bot.AuthorizedUsers {
if user != message.From.UserName { if user != message.From.UserName {
if _, ok := bot.Telegram.ChatDB.Db[user]; !ok { if _, ok := bot.ChatDB.Db[user]; !ok {
log.Printf("[%s] The chat db does not have any mapping for %s, skipping...", message.From.UserName, user) log.Printf("[%s] The chat db does not have any mapping for %s, skipping...", message.From.UserName, user)
continue continue
} }
msg := tgbotapi.NewForward(bot.Telegram.ChatDB.Db[user], message.Chat.ID, message.MessageID) msg := tgbotapi.NewForward(bot.ChatDB.Db[user], message.Chat.ID, message.MessageID)
_, err := bot.Telegram.API.Send(msg) _, err := bot.API.Send(msg)
if err != nil { if err != nil {
log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, bot.Telegram.ChatDB.Db[user]) log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, bot.ChatDB.Db[user])
} }
} }
} }
} }
func (bot *PhotoBot) getFile(message *tgbotapi.Message, telegramFileId string, mediaStoreId string) error { func (bot *TelegramBot) getFile(message *tgbotapi.Message, telegramFileId string, mediaStoreId string) error {
url, err := bot.Telegram.API.GetFileDirectURL(telegramFileId) url, err := bot.API.GetFileDirectURL(telegramFileId)
if err != nil { if err != nil {
return err return err
} }
@ -250,7 +243,7 @@ func (bot *PhotoBot) getFile(message *tgbotapi.Message, telegramFileId string, m
return nil return nil
} }
func (bot *PhotoBot) handlePhoto(message *tgbotapi.Message) error { func (bot *TelegramBot) handlePhoto(message *tgbotapi.Message) error {
// Find the best resolution among all available sizes // Find the best resolution among all available sizes
fileId := "" fileId := ""
maxWidth := 0 maxWidth := 0
@ -274,7 +267,7 @@ func (bot *PhotoBot) handlePhoto(message *tgbotapi.Message) error {
t := time.Unix(int64(message.Date), 0) t := time.Unix(int64(message.Date), 0)
return bot.MediaStore.CommitPhoto(mediaStoreId, t, message.Caption) return bot.MediaStore.CommitPhoto(mediaStoreId, t, message.Caption)
} }
func (bot *PhotoBot) handleVideo(message *tgbotapi.Message) error { func (bot *TelegramBot) handleVideo(message *tgbotapi.Message) error {
// Get a unique id // Get a unique id
mediaStoreId := bot.MediaStore.GetUniqueID() mediaStoreId := bot.MediaStore.GetUniqueID()
@ -295,20 +288,20 @@ func (bot *PhotoBot) handleVideo(message *tgbotapi.Message) error {
return bot.MediaStore.CommitVideo(mediaStoreId, t, message.Caption) return bot.MediaStore.CommitVideo(mediaStoreId, t, message.Caption)
} }
func (bot *PhotoBot) handleHelpCommand(message *tgbotapi.Message) { func (bot *TelegramBot) handleHelpCommand(message *tgbotapi.Message) {
bot.Telegram.replyWithMessage(message, bot.Telegram.Messages.Help) bot.replyWithMessage(message, bot.Messages.Help)
} }
func (bot *PhotoBot) handleShareCommand(message *tgbotapi.Message) { func (bot *TelegramBot) handleShareCommand(message *tgbotapi.Message) {
albumList, err := bot.MediaStore.ListAlbums() albumList, err := bot.MediaStore.ListAlbums()
if err != nil { if err != nil {
log.Printf("[%s] cannot get album list: %s", message.From.UserName, err) log.Printf("[%s] cannot get album list: %s", message.From.UserName, err)
bot.Telegram.replyToCommandWithMessage(message, bot.Telegram.Messages.ServerError) bot.replyToCommandWithMessage(message, bot.Messages.ServerError)
return return
} }
var text strings.Builder var text strings.Builder
text.WriteString(fmt.Sprintf(bot.Telegram.Messages.SharedAlbum, bot.Telegram.PerAlbumTokenValidity)) text.WriteString(fmt.Sprintf(bot.Messages.SharedAlbum, bot.PerAlbumTokenValidity))
text.WriteString("\n") text.WriteString("\n")
sort.Sort(sort.Reverse(albumList)) sort.Sort(sort.Reverse(albumList))
var tokenData TokenData = TokenData{ var tokenData TokenData = TokenData{
@ -323,80 +316,80 @@ func (bot *PhotoBot) handleShareCommand(message *tgbotapi.Message) {
title = title + " 🔥" title = title + " 🔥"
} }
tokenData.Entitlement = id tokenData.Entitlement = id
token := bot.Telegram.TokenGenerator.NewToken(tokenData) token := bot.TokenGenerator.NewToken(tokenData)
url := fmt.Sprintf("%s/s/%s/%s/album/%s/", bot.Telegram.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token), url.PathEscape(id)) url := fmt.Sprintf("%s/s/%s/%s/album/%s/", bot.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token), url.PathEscape(id))
text.WriteString(fmt.Sprintf("- [%s %s](%s)\n", album.Date.Format("2006-01"), title, url)) text.WriteString(fmt.Sprintf("- [%s %s](%s)\n", album.Date.Format("2006-01"), title, url))
} }
bot.Telegram.replyWithMarkdownMessage(message, text.String()) bot.replyWithMarkdownMessage(message, text.String())
} }
func (bot *PhotoBot) handleBrowseCommand(message *tgbotapi.Message) { func (bot *TelegramBot) handleBrowseCommand(message *tgbotapi.Message) {
var tokenData TokenData = TokenData{ var tokenData TokenData = TokenData{
Timestamp: time.Now(), Timestamp: time.Now(),
Username: message.From.UserName, Username: message.From.UserName,
} }
// Global share // Global share
token := bot.Telegram.TokenGenerator.NewToken(tokenData) token := bot.TokenGenerator.NewToken(tokenData)
url := fmt.Sprintf("%s/s/%s/%s/album/", bot.Telegram.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token)) url := fmt.Sprintf("%s/s/%s/%s/album/", bot.WebPublicURL, url.PathEscape(message.From.UserName), url.PathEscape(token))
bot.Telegram.replyWithMessage(message, fmt.Sprintf(bot.Telegram.Messages.SharedGlobal, bot.Telegram.GlobalTokenValidity)) bot.replyWithMessage(message, fmt.Sprintf(bot.Messages.SharedGlobal, bot.GlobalTokenValidity))
bot.Telegram.replyWithMessage(message, url) bot.replyWithMessage(message, url)
} }
func (bot *PhotoBot) handleInfoCommand(message *tgbotapi.Message) { func (bot *TelegramBot) handleInfoCommand(message *tgbotapi.Message) {
album, err := bot.MediaStore.GetCurrentAlbum() album, err := bot.MediaStore.GetCurrentAlbum()
if err != nil { if err != nil {
log.Printf("[%s] cannot get current album: %s", message.From.UserName, err) log.Printf("[%s] cannot get current album: %s", message.From.UserName, err)
bot.Telegram.replyToCommandWithMessage(message, bot.Telegram.Messages.ServerError) bot.replyToCommandWithMessage(message, bot.Messages.ServerError)
return return
} }
if album.Title != "" { if album.Title != "" {
bot.Telegram.replyWithMessage(message, fmt.Sprintf(bot.Telegram.Messages.Info, album.Title)) bot.replyWithMessage(message, fmt.Sprintf(bot.Messages.Info, album.Title))
} else { } else {
bot.Telegram.replyWithMessage(message, bot.Telegram.Messages.InfoNoAlbum) bot.replyWithMessage(message, bot.Messages.InfoNoAlbum)
} }
} }
func (bot *PhotoBot) handleNewAlbumCommand(message *tgbotapi.Message) { func (bot *TelegramBot) handleNewAlbumCommand(message *tgbotapi.Message) {
bot.Telegram.replyWithForcedReply(message, bot.Telegram.Messages.MissingAlbumName) bot.replyWithForcedReply(message, bot.Messages.MissingAlbumName)
} }
func (bot *PhotoBot) handleNewAlbumCommandReply(message *tgbotapi.Message) { func (bot *TelegramBot) handleNewAlbumCommandReply(message *tgbotapi.Message) {
albumName := message.Text albumName := message.Text
err := bot.MediaStore.NewAlbum(albumName) err := bot.MediaStore.NewAlbum(albumName)
if err != nil { if err != nil {
log.Printf("[%s] cannot create album '%s': %s", message.From.UserName, albumName, err) log.Printf("[%s] cannot create album '%s': %s", message.From.UserName, albumName, err)
bot.Telegram.replyToCommandWithMessage(message, bot.Telegram.Messages.ServerError) bot.replyToCommandWithMessage(message, bot.Messages.ServerError)
return return
} }
bot.Telegram.replyWithMessage(message, bot.Telegram.Messages.AlbumCreated) bot.replyWithMessage(message, bot.Messages.AlbumCreated)
} }
func (telegram *TelegramBackend) replyToCommandWithMessage(message *tgbotapi.Message, text string) error { func (telegram *TelegramBot) replyToCommandWithMessage(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text) msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ReplyToMessageID = message.MessageID msg.ReplyToMessageID = message.MessageID
_, err := telegram.API.Send(msg) _, err := telegram.API.Send(msg)
return err return err
} }
func (telegram *TelegramBackend) replyWithMessage(message *tgbotapi.Message, text string) error { func (telegram *TelegramBot) replyWithMessage(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text) msg := tgbotapi.NewMessage(message.Chat.ID, text)
_, err := telegram.API.Send(msg) _, err := telegram.API.Send(msg)
return err return err
} }
func (telegram *TelegramBackend) replyWithMarkdownMessage(message *tgbotapi.Message, text string) error { func (telegram *TelegramBot) replyWithMarkdownMessage(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text) msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ParseMode = tgbotapi.ModeMarkdown msg.ParseMode = tgbotapi.ModeMarkdown
_, err := telegram.API.Send(msg) _, err := telegram.API.Send(msg)
return err return err
} }
func (telegram *TelegramBackend) replyWithForcedReply(message *tgbotapi.Message, text string) error { func (telegram *TelegramBot) replyWithForcedReply(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text) msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ReplyMarkup = tgbotapi.ForceReply{ msg.ReplyMarkup = tgbotapi.ForceReply{
ForceReply: true, ForceReply: true,

33
http.go

@ -5,8 +5,6 @@ import (
"net/http" "net/http"
"path" "path"
"strings" "strings"
"github.com/rakyll/statik/fs"
) )
// ShiftPath splits off the first component of p, which will be cleaned of // ShiftPath splits off the first component of p, which will be cleaned of
@ -25,34 +23,11 @@ func ShiftPath(p string) (head, tail string) {
return p[1:i], p[i:] return p[1:i], p[i:]
} }
func (bot *PhotoBot) ServeWebInterface(listenAddr string, frontend *SecurityFrontend) { func ServeWebInterface(listenAddr string, webInterface http.Handler, staticFiles http.FileSystem) {
statikFS, err := fs.New()
if err != nil {
log.Fatal(err)
}
bot.WebInterface.AlbumTemplate, err = getTemplate(statikFS, "/album.html.template", "album")
if err != nil {
log.Fatal(err)
}
bot.WebInterface.MediaTemplate, err = getTemplate(statikFS, "/media.html.template", "media")
if err != nil {
log.Fatal(err)
}
bot.WebInterface.IndexTemplate, err = getTemplate(statikFS, "/index.html.template", "index")
if err != nil {
log.Fatal(err)
}
router := http.NewServeMux() router := http.NewServeMux()
router.Handle("/js/", http.FileServer(statikFS)) router.Handle("/js/", http.FileServer(staticFiles))
router.Handle("/css/", http.FileServer(statikFS)) router.Handle("/css/", http.FileServer(staticFiles))
router.Handle("/", webInterface)
// Put the Web Interface behind the security frontend
frontend.Protected = bot
router.Handle("/", frontend)
server := &http.Server{ server := &http.Server{
Addr: listenAddr, Addr: listenAddr,

109
main.go

@ -11,6 +11,8 @@ import (
"path/filepath" "path/filepath"
"time" "time"
_ "github.com/nmasse-itix/Telegram-Photo-Album-Bot/statik"
"github.com/rakyll/statik/fs"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
@ -122,10 +124,6 @@ func validateConfig() {
log.Fatal("No OpenID Connect Client Secret provided!") log.Fatal("No OpenID Connect Client Secret provided!")
} }
if viper.GetString("WebInterface.OIDC.RedirectURL") == "" {
log.Fatal("No OpenID Connect Redirect URL provided!")
}
if viper.GetString("WebInterface.OIDC.ClientSecret") == "" { if viper.GetString("WebInterface.OIDC.ClientSecret") == "" {
log.Fatal("No OpenID Connect Client Secret provided!") log.Fatal("No OpenID Connect Client Secret provided!")
} }
@ -173,24 +171,24 @@ func getMessagesFromConfig() TelegramMessages {
} }
} }
func getSecretKey(configKey string, minLength int) []byte {
key, err := base64.StdEncoding.DecodeString(viper.GetString(configKey))
if err != nil {
panic(fmt.Sprintf("%s: %s", configKey, err))
}
if len(key) < 32 {
panic(fmt.Sprintf("%s: The given token generator authentication key is too short (got %d bytes, expected at least %d)!", configKey, len(key), minLength))
}
return key
}
func main() { func main() {
initConfig() initConfig()
validateConfig() validateConfig()
// Create the Bot // Make sure the needed folder structure exists in the target folder
photoBot := InitBot(viper.GetString("TargetDir"))
photoBot.Telegram.RetryDelay = time.Duration(viper.GetInt("Telegram.RetryDelay")) * time.Second
photoBot.Telegram.NewUpdateTimeout = viper.GetInt("Telegram.NewUpdateTimeout")
photoBot.Telegram.Commands = getCommandsFromConfig()
photoBot.Telegram.Messages = getMessagesFromConfig()
photoBot.WebInterface.SiteName = viper.GetString("WebInterface.SiteName")
photoBot.Telegram.WebPublicURL = viper.GetString("WebInterface.PublicURL")
// Fill the authorized users
for _, item := range viper.GetStringSlice("Telegram.AuthorizedUsers") {
photoBot.Telegram.AuthorizedUsers[item] = true
}
targetDir := viper.GetString("TargetDir") targetDir := viper.GetString("TargetDir")
for _, dir := range []string{"data", "db"} { for _, dir := range []string{"data", "db"} {
fullPath := filepath.Join(targetDir, dir) fullPath := filepath.Join(targetDir, dir)
@ -200,63 +198,69 @@ func main() {
} }
} }
// Create the ChatDB and inject it // Create the MediaStore
chatDB, err := InitChatDB(filepath.Join(targetDir, "db", "chatdb.yaml")) mediaStore, err := InitMediaStore(filepath.Join(targetDir, "data"))
if err != nil { if err != nil {
panic(err) panic(err)
} }
photoBot.Telegram.ChatDB = chatDB
// Create the MediaStore and inject it // Create the Token Generator
mediaStore, err := InitMediaStore(filepath.Join(targetDir, "data")) tokenAuthenticationKey := getSecretKey("Telegram.TokenGenerator.AuthenticationKey", 32)
tokenGenerator, err := NewTokenGenerator(tokenAuthenticationKey, crypto.SHA256)
if err != nil { if err != nil {
panic(err) panic(err)
} }
// Create the ChatDB
chatDB, err := InitChatDB(filepath.Join(targetDir, "db", "chatdb.yaml"))
if err != nil {
panic(err)
}
// Create the Bot
photoBot := NewTelegramBot()
photoBot.RetryDelay = time.Duration(viper.GetInt("Telegram.RetryDelay")) * time.Second
photoBot.NewUpdateTimeout = viper.GetInt("Telegram.NewUpdateTimeout")
photoBot.Commands = getCommandsFromConfig()
photoBot.Messages = getMessagesFromConfig()
photoBot.WebPublicURL = viper.GetString("WebInterface.PublicURL")
photoBot.MediaStore = mediaStore photoBot.MediaStore = mediaStore
photoBot.ChatDB = chatDB
photoBot.TokenGenerator = tokenGenerator
photoBot.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity")
photoBot.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity")
// Fill the authorized users
for _, item := range viper.GetStringSlice("Telegram.AuthorizedUsers") {
photoBot.AuthorizedUsers[item] = true
}
// Start the bot // Start the bot
photoBot.StartBot(viper.GetString("Telegram.Token")) photoBot.StartBot(viper.GetString("Telegram.Token"), viper.GetBool("Telegram.Debug"))
photoBot.Telegram.API.Debug = viper.GetBool("Telegram.Debug")
// Token Generator // Setup the web interface
tokenAuthenticationKey, err := base64.StdEncoding.DecodeString(viper.GetString("Telegram.TokenGenerator.AuthenticationKey")) statikFS, err := fs.New()
if err != nil { if err != nil {
panic(err) panic(err)
} }
if len(tokenAuthenticationKey) < 32 { web, err := NewWebInterface(statikFS)
panic("The given token generator authentication key is too short!")
}
tokenGenerator, err := NewTokenGenerator(tokenAuthenticationKey, crypto.SHA256)
if err != nil { if err != nil {
panic(err) panic(err)
} }
photoBot.Telegram.TokenGenerator = tokenGenerator web.MediaStore = mediaStore
photoBot.Telegram.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity") web.SiteName = viper.GetString("WebInterface.SiteName")
photoBot.Telegram.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity")
// Setup the web interface // Setup the security frontend
var oidc OpenIdSettings = OpenIdSettings{ var oidc OpenIdSettings = OpenIdSettings{
ClientID: viper.GetString("WebInterface.OIDC.ClientID"), ClientID: viper.GetString("WebInterface.OIDC.ClientID"),
ClientSecret: viper.GetString("WebInterface.OIDC.ClientSecret"), ClientSecret: viper.GetString("WebInterface.OIDC.ClientSecret"),
DiscoveryUrl: viper.GetString("WebInterface.OIDC.DiscoveryUrl"), DiscoveryUrl: viper.GetString("WebInterface.OIDC.DiscoveryUrl"),
RedirectURL: viper.GetString("WebInterface.OIDC.RedirectURL"), RedirectURL: GetOAuthCallbackURL(viper.GetString("WebInterface.PublicURL")),
GSuiteDomain: viper.GetString("WebInterface.OIDC.GSuiteDomain"), GSuiteDomain: viper.GetString("WebInterface.OIDC.GSuiteDomain"),
Scopes: viper.GetStringSlice("WebInterface.OIDC.Scopes"), Scopes: viper.GetStringSlice("WebInterface.OIDC.Scopes"),
} }
authenticationKey, err := base64.StdEncoding.DecodeString(viper.GetString("WebInterface.Sessions.AuthenticationKey")) authenticationKey := getSecretKey("WebInterface.Sessions.AuthenticationKey", 32)
if err != nil { encryptionKey := getSecretKey("WebInterface.Sessions.EncryptionKey", 32)
panic(err)
}
if len(authenticationKey) < 32 {
panic("The given session authentication key is too short!")
}
encryptionKey, err := base64.StdEncoding.DecodeString(viper.GetString("WebInterface.Sessions.EncryptionKey"))
if err != nil {
panic(err)
}
if len(encryptionKey) < 32 {
panic("The given session encryption key is too short!")
}
var sessions SessionSettings = SessionSettings{ var sessions SessionSettings = SessionSettings{
AuthenticationKey: authenticationKey, AuthenticationKey: authenticationKey,
EncryptionKey: encryptionKey, EncryptionKey: encryptionKey,
@ -270,7 +274,10 @@ func main() {
securityFrontend.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity") securityFrontend.GlobalTokenValidity = viper.GetInt("Telegram.TokenGenerator.GlobalValidity")
securityFrontend.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity") securityFrontend.PerAlbumTokenValidity = viper.GetInt("Telegram.TokenGenerator.PerAlbumValidity")
// Put the Web Interface behind the security frontend
securityFrontend.Protected = web
initLogFile() initLogFile()
go photoBot.Process() go photoBot.Process()
photoBot.ServeWebInterface(viper.GetString("WebInterface.Listen"), securityFrontend) ServeWebInterface(viper.GetString("WebInterface.Listen"), securityFrontend, statikFS)
} }

17
security.go

@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"log" "log"
"net/http" "net/http"
"net/url"
"strings" "strings"
"time" "time"
@ -46,6 +47,20 @@ func init() {
gob.Register(&WebUser{}) gob.Register(&WebUser{})
} }
func GetOAuthCallbackURL(publicUrl string) string {
u, err := url.Parse(publicUrl)
if err != nil {
// If the URL cannot be parsed, use it as-is
return publicUrl
}
u.Path = "/oauth/callback"
u.Fragment = ""
u.RawQuery = ""
return u.String()
}
func NewSecurityFrontend(openidSettings OpenIdSettings, sessionSettings SessionSettings, tokenGenerator *TokenGenerator) (*SecurityFrontend, error) { func NewSecurityFrontend(openidSettings OpenIdSettings, sessionSettings SessionSettings, tokenGenerator *TokenGenerator) (*SecurityFrontend, error) {
var securityFrontend SecurityFrontend var securityFrontend SecurityFrontend
provider, err := oidc.NewProvider(context.TODO(), openidSettings.DiscoveryUrl) provider, err := oidc.NewProvider(context.TODO(), openidSettings.DiscoveryUrl)
@ -289,6 +304,7 @@ func (securityFrontend *SecurityFrontend) handleTelegramTokenAuthentication(w ht
Timestamp: time.Now(), Timestamp: time.Now(),
Entitlement: album, Entitlement: album,
} }
// try to validate the token with an album entitlement
ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.PerAlbumTokenValidity) ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.PerAlbumTokenValidity)
if err != nil { if err != nil {
http.Error(w, "Invalid Token", http.StatusBadRequest) http.Error(w, "Invalid Token", http.StatusBadRequest)
@ -296,6 +312,7 @@ func (securityFrontend *SecurityFrontend) handleTelegramTokenAuthentication(w ht
} }
if !ok { if !ok {
// if it fails, it may be a global token
data.Entitlement = "" data.Entitlement = ""
ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.GlobalTokenValidity) ok, err := securityFrontend.TokenGenerator.ValidateToken(data, token, securityFrontend.GlobalTokenValidity)
if !ok || err != nil { if !ok || err != nil {

81
web.go

@ -8,17 +8,38 @@ import (
"sort" "sort"
"strings" "strings"
"time" "time"
_ "github.com/nmasse-itix/Telegram-Photo-Album-Bot/statik"
) )
type WebInterface struct { type WebInterface struct {
MediaStore *MediaStore
AlbumTemplate *template.Template AlbumTemplate *template.Template
MediaTemplate *template.Template MediaTemplate *template.Template
IndexTemplate *template.Template IndexTemplate *template.Template
SiteName string SiteName string
} }
func NewWebInterface(statikFS http.FileSystem) (*WebInterface, error) {
var err error
web := WebInterface{}
web.AlbumTemplate, err = getTemplate(statikFS, "/album.html.template", "album")
if err != nil {
return nil, err
}
web.MediaTemplate, err = getTemplate(statikFS, "/media.html.template", "media")
if err != nil {
return nil, err
}
web.IndexTemplate, err = getTemplate(statikFS, "/index.html.template", "index")
if err != nil {
return nil, err
}
return &web, nil
}
func slurpFile(statikFS http.FileSystem, filename string) (string, error) { func slurpFile(statikFS http.FileSystem, filename string) (string, error) {
fd, err := statikFS.Open(filename) fd, err := statikFS.Open(filename)
if err != nil { if err != nil {
@ -66,99 +87,99 @@ func getTemplate(statikFS http.FileSystem, filename string, name string) (*templ
return tmpl.Funcs(customFunctions).Parse(content) return tmpl.Funcs(customFunctions).Parse(content)
} }
func (bot *PhotoBot) HandleFileNotFound(w http.ResponseWriter, r *http.Request) { func (web *WebInterface) handleFileNotFound(w http.ResponseWriter, r *http.Request) {
http.Error(w, "File not found", http.StatusNotFound) http.Error(w, "File not found", http.StatusNotFound)
} }
func (bot *PhotoBot) HandleError(w http.ResponseWriter, r *http.Request) { func (web *WebInterface) handleError(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal server error", http.StatusInternalServerError) http.Error(w, "Internal server error", http.StatusInternalServerError)
} }
func (bot *PhotoBot) HandleDisplayAlbum(w http.ResponseWriter, r *http.Request, albumName string) { func (web *WebInterface) handleDisplayAlbum(w http.ResponseWriter, r *http.Request, albumName string) {
if albumName == "latest" { if albumName == "latest" {
albumName = "" albumName = ""
} }
album, err := bot.MediaStore.GetAlbum(albumName, false) album, err := web.MediaStore.GetAlbum(albumName, false)
if err != nil { if err != nil {
log.Printf("MediaStore.GetAlbum: %s", err) log.Printf("MediaStore.GetAlbum: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
err = bot.WebInterface.AlbumTemplate.Execute(w, album) err = web.AlbumTemplate.Execute(w, album)
if err != nil { if err != nil {
log.Printf("Template.Execute: %s", err) log.Printf("Template.Execute: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
} }
func (bot *PhotoBot) HandleDisplayIndex(w http.ResponseWriter, r *http.Request) { func (web *WebInterface) handleDisplayIndex(w http.ResponseWriter, r *http.Request) {
albums, err := bot.MediaStore.ListAlbums() albums, err := web.MediaStore.ListAlbums()
if err != nil { if err != nil {
log.Printf("MediaStore.ListAlbums: %s", err) log.Printf("MediaStore.ListAlbums: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
sort.Sort(sort.Reverse(albums)) sort.Sort(sort.Reverse(albums))
err = bot.WebInterface.IndexTemplate.Execute(w, struct { err = web.IndexTemplate.Execute(w, struct {
Title string Title string
Albums []Album Albums []Album
}{ }{
bot.WebInterface.SiteName, web.SiteName,
albums, albums,
}) })
if err != nil { if err != nil {
log.Printf("Template.Execute: %s", err) log.Printf("Template.Execute: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
} }
func (bot *PhotoBot) HandleDisplayMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaId string) { func (web *WebInterface) handleDisplayMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaId string) {
if albumName == "latest" { if albumName == "latest" {
albumName = "" albumName = ""
} }
media, err := bot.MediaStore.GetMedia(albumName, mediaId) media, err := web.MediaStore.GetMedia(albumName, mediaId)
if err != nil { if err != nil {
log.Printf("MediaStore.GetMedia: %s", err) log.Printf("MediaStore.GetMedia: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
if media == nil { if media == nil {
bot.HandleFileNotFound(w, r) web.handleFileNotFound(w, r)
return return
} }
err = bot.WebInterface.MediaTemplate.Execute(w, media) err = web.MediaTemplate.Execute(w, media)
if err != nil { if err != nil {
log.Printf("Template.Execute: %s", err) log.Printf("Template.Execute: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
} }
func (bot *PhotoBot) HandleGetMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaFilename string) { func (web *WebInterface) handleGetMedia(w http.ResponseWriter, r *http.Request, albumName string, mediaFilename string) {
if albumName == "latest" { if albumName == "latest" {
albumName = "" albumName = ""
} }
fd, modtime, err := bot.MediaStore.OpenFile(albumName, mediaFilename) fd, modtime, err := web.MediaStore.OpenFile(albumName, mediaFilename)
if err != nil { if err != nil {
log.Printf("MediaStore.OpenFile: %s", err) log.Printf("MediaStore.OpenFile: %s", err)
bot.HandleError(w, r) web.handleError(w, r)
return return
} }
defer fd.Close() defer fd.Close()
http.ServeContent(w, r, mediaFilename, modtime, fd) http.ServeContent(w, r, mediaFilename, modtime, fd)
} }
func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (web *WebInterface) ServeHTTP(w http.ResponseWriter, r *http.Request) {
originalPath := r.URL.Path originalPath := r.URL.Path
var resource string var resource string
resource, r.URL.Path = ShiftPath(r.URL.Path) resource, r.URL.Path = ShiftPath(r.URL.Path)
@ -179,13 +200,13 @@ func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently)
return return
} }
bot.HandleDisplayAlbum(w, r, albumName) web.handleDisplayAlbum(w, r, albumName)
return return
} else if kind == "raw" && media != "" { } else if kind == "raw" && media != "" {
bot.HandleGetMedia(w, r, albumName, media) web.handleGetMedia(w, r, albumName, media)
return return
} else if kind == "media" && media != "" { } else if kind == "media" && media != "" {
bot.HandleDisplayMedia(w, r, albumName, media) web.handleDisplayMedia(w, r, albumName, media)
return return
} }
} else { } else {
@ -193,7 +214,7 @@ func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently) http.Redirect(w, r, originalPath+"/", http.StatusMovedPermanently)
return return
} }
bot.HandleDisplayIndex(w, r) web.handleDisplayIndex(w, r)
return return
} }
} else if resource == "" { } else if resource == "" {
@ -201,5 +222,5 @@ func (bot *PhotoBot) ServeHTTP(w http.ResponseWriter, r *http.Request) {
return return
} }
bot.HandleFileNotFound(w, r) web.handleFileNotFound(w, r)
} }

Loading…
Cancel
Save