Browse Source

rework

master
Nicolas Massé 6 years ago
parent
commit
c328adb971
  1. 301
      bot.go
  2. 61
      chatdb.go
  3. 58
      chatdb_test.go
  4. 15
      go.mod
  5. 159
      go.sum
  6. 554
      main.go
  7. 180
      media_store.go
  8. 29
      media_store_test.go
  9. 24
      test.go

301
bot.go

@ -0,0 +1,301 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"time"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/spf13/viper"
)
type PhotoBot struct {
Telegram TelegramBackend
MediaStore *MediaStore
}
type TelegramBackend struct {
ChatDB *ChatDB
AuthorizedUsers map[string]bool
RetryDelay time.Duration
NewUpdateTimeout int
API *tgbotapi.BotAPI
}
func InitBot(targetDir string) *PhotoBot {
return &PhotoBot{
Telegram: TelegramBackend{
AuthorizedUsers: make(map[string]bool),
RetryDelay: time.Duration(30) * time.Second,
},
}
}
func (bot *PhotoBot) StartBot(token string) {
var telegramBot *tgbotapi.BotAPI
var err error
for tryAgain := true; tryAgain; tryAgain = (err != nil) {
telegramBot, err = tgbotapi.NewBotAPI(token)
if err != nil {
log.Printf("Cannot start the Telegram Bot because of '%s'. Retrying in %d seconds...", err, bot.Telegram.RetryDelay/time.Second)
time.Sleep(bot.Telegram.RetryDelay)
}
}
log.Printf("Authorized on account %s", telegramBot.Self.UserName)
bot.Telegram.API = telegramBot
}
func (bot *PhotoBot) Process() {
u := tgbotapi.NewUpdate(0)
u.Timeout = bot.Telegram.NewUpdateTimeout
updates, _ := bot.Telegram.API.GetUpdatesChan(u)
for update := range updates {
bot.ProcessUpdate(update)
}
}
func (bot *PhotoBot) ProcessUpdate(update tgbotapi.Update) {
if update.Message == nil || update.Message.From == nil {
return
}
text := update.Message.Text
username := update.Message.From.UserName
if username == "" {
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgNoUsername"))
return
}
if !bot.Telegram.AuthorizedUsers[username] {
log.Printf("[%s] unauthorized user", username)
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgForbidden"))
return
}
err := bot.Telegram.ChatDB.UpdateWith(username, update.Message.Chat.ID)
if err != nil {
log.Printf("[%s] cannot update chat db: %s", username, err)
}
if text != "" {
if update.Message.IsCommand() {
log.Printf("[%s] command: %s", username, text)
switch update.Message.Command() {
case "start", "aide", "help":
bot.handleHelpCommand(update.Message)
case "nouvelAlbum":
bot.handleNewAlbumCommand(update.Message)
case "info":
bot.handleInfoCommand(update.Message)
case "pourLouise":
bot.Telegram.replyWithForcedReply(update.Message, viper.GetString("MsgSendMeSomething"))
default:
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgDoNotUnderstand"))
}
} else {
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgDoNotUnderstand"))
}
} else if update.Message.Photo != nil {
err := bot.handlePhoto(update.Message)
if err != nil {
log.Printf("[%s] cannot add photo to current album: %s", username, err)
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgServerError"))
return
}
bot.dispatchMessage(update.Message)
bot.Telegram.replyWithMessage(update.Message, viper.GetString("MsgThankYouMedia"))
} else if update.Message.Video != nil {
err := bot.handleVideo(update.Message)
if err != nil {
log.Printf("[%s] cannot add video to current album: %s", username, err)
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgServerError"))
return
}
bot.dispatchMessage(update.Message)
bot.Telegram.replyWithMessage(update.Message, viper.GetString("MsgThankYouMedia"))
} else {
log.Printf("[%s] cannot handle this type of message", username)
bot.Telegram.replyToCommandWithMessage(update.Message, viper.GetString("MsgDoNotUnderstand"))
}
}
func (bot *PhotoBot) dispatchMessage(message *tgbotapi.Message) {
for user, _ := range bot.Telegram.AuthorizedUsers {
if user != message.From.UserName {
if _, ok := bot.Telegram.ChatDB.Db[user]; !ok {
log.Printf("[%s] The chat db does not have any mapping for %s, skipping...", message.From.UserName, user)
continue
}
msg := tgbotapi.NewForward(bot.Telegram.ChatDB.Db[user], message.Chat.ID, message.MessageID)
_, err := bot.Telegram.API.Send(msg)
if err != nil {
log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, bot.Telegram.ChatDB.Db[user])
}
}
}
}
func (bot *PhotoBot) getFile(message *tgbotapi.Message, telegramFileId string, mediaStoreId string) error {
url, err := bot.Telegram.API.GetFileDirectURL(telegramFileId)
if err != nil {
return err
}
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
// Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)
n, err := resp.Body.Read(buffer)
if err != nil {
return err
}
// Detect the content-type
contentType := http.DetectContentType(buffer)
var extension string
if contentType == "image/jpeg" {
extension = ".jpeg"
} else if contentType == "video/mp4" {
extension = ".mp4"
} else {
log.Printf("[%s] Unknown media content-type '%s'", message.From.UserName, contentType)
extension = ".bin"
}
// Create the file
out, err := bot.MediaStore.AddFile(mediaStoreId + extension)
if err != nil {
return err
}
defer out.Close()
// Write back the first 512 bytes
n, err = out.Write(buffer[0:n])
if err != nil {
return err
}
// Write the rest of the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return err
}
return nil
}
func (bot *PhotoBot) handlePhoto(message *tgbotapi.Message) error {
// Find the best resolution among all available sizes
fileId := ""
maxWidth := 0
for _, photo := range *message.Photo {
if photo.Width > maxWidth {
fileId = photo.FileID
maxWidth = photo.Width
}
}
// Get a unique id
mediaStoreId := bot.MediaStore.GetUniqueID()
// Download the photo from the Telegram API and save it in the MediaStore
err := bot.getFile(message, fileId, mediaStoreId)
if err != nil {
return err
}
// parse the message timestamp
t := time.Unix(int64(message.Date), 0)
return bot.MediaStore.CommitPhoto(mediaStoreId, t, message.Caption)
}
func (bot *PhotoBot) handleVideo(message *tgbotapi.Message) error {
// Get a unique id
mediaStoreId := bot.MediaStore.GetUniqueID()
// Download the video from the Telegram API and save it in the MediaStore
err := bot.getFile(message, message.Video.FileID, mediaStoreId)
if err != nil {
return err
}
// Download the video thumbnail from the Telegram API and save it in the MediaStore
err = bot.getFile(message, message.Video.Thumbnail.FileID, mediaStoreId)
if err != nil {
log.Printf("[%s] Cannot download video thumbnail: %s", message.From.UserName, err)
}
// parse the message timestamp
t := time.Unix(int64(message.Date), 0)
return bot.MediaStore.CommitVideo(mediaStoreId, t, message.Caption)
}
func (bot *PhotoBot) handleHelpCommand(message *tgbotapi.Message) {
bot.Telegram.replyWithMessage(message, viper.GetString("MsgHelp"))
}
func (bot *PhotoBot) handleInfoCommand(message *tgbotapi.Message) {
albumName, err := bot.MediaStore.GetCurrentAlbum()
if err != nil {
log.Printf("[%s] cannot get current album: %s", message.From.UserName, err)
bot.Telegram.replyToCommandWithMessage(message, viper.GetString("MsgServerError"))
return
}
if albumName != "" {
bot.Telegram.replyWithMessage(message, fmt.Sprintf(viper.GetString("MsgInfo"), albumName))
} else {
bot.Telegram.replyWithMessage(message, viper.GetString("MsgInfoNoAlbum"))
}
}
func (bot *PhotoBot) handleNewAlbumCommand(message *tgbotapi.Message) {
if len(message.Text) < 14 {
bot.Telegram.replyToCommandWithMessage(message, viper.GetString("MsgMissingAlbumName"))
return
}
albumName := message.CommandArguments()
err := bot.MediaStore.NewAlbum(albumName)
if err != nil {
log.Printf("[%s] cannot create album '%s': %s", message.From.UserName, albumName, err)
bot.Telegram.replyToCommandWithMessage(message, viper.GetString("MsgServerError"))
return
}
bot.Telegram.replyWithMessage(message, viper.GetString("MsgAlbumCreated"))
}
func (telegram *TelegramBackend) replyToCommandWithMessage(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ReplyToMessageID = message.MessageID
_, err := telegram.API.Send(msg)
return err
}
func (telegram *TelegramBackend) replyWithMessage(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
_, err := telegram.API.Send(msg)
return err
}
func (telegram *TelegramBackend) replyWithForcedReply(message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ReplyMarkup = tgbotapi.ForceReply{
ForceReply: true,
Selective: true,
}
_, err := telegram.API.Send(msg)
return err
}

61
chatdb.go

@ -0,0 +1,61 @@
package main
import (
"io/ioutil"
"log"
"os"
"gopkg.in/yaml.v2"
)
type ChatDB struct {
Path string
// Map usernames to chat id
Db map[string]int64
}
func InitChatDB(path string) (*ChatDB, error) {
db := make(map[string]int64)
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE, 0600)
if err != nil {
return nil, err
}
defer f.Close()
yamlData, err := ioutil.ReadAll(f)
if err != nil {
return nil, err
}
err = yaml.Unmarshal(yamlData, &db)
if err != nil {
return nil, err
}
return &ChatDB{Path: path, Db: db}, nil
}
func (chatdb *ChatDB) UpdateWith(username string, chatId int64) error {
if _, ok := chatdb.Db[username]; !ok {
chatdb.Db[username] = chatId
yamlData, err := yaml.Marshal(chatdb.Db)
if err != nil {
return err
}
err = os.Rename(chatdb.Path, chatdb.Path+".bak")
if err != nil {
log.Printf("Cannot perform a backup of the chatdb before update: %s", err)
}
err = ioutil.WriteFile(chatdb.Path, yamlData, 0600)
if err != nil {
return err
}
}
return nil
}

58
chatdb_test.go

@ -0,0 +1,58 @@
package main
import (
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/magiconair/properties/assert"
)
func TestInitChatDB(t *testing.T) {
tmp := createTempDir(t)
defer tmp.cleanup(t)
file := filepath.Join(tmp.RootDir, "chat.yaml")
_, err := InitChatDB(file)
if err != nil {
t.Errorf("InitChatDB(): %s", err)
}
_, err = os.Stat(file)
if err != nil {
t.Errorf("InitChatDB(): chatdb not created (error = %s)", err)
}
}
func TestUpdateWith(t *testing.T) {
tmp := createTempDir(t)
defer tmp.cleanup(t)
file := filepath.Join(tmp.RootDir, "chat.yaml")
chatdb, err := InitChatDB(file)
if err != nil {
t.Errorf("InitChatDB(): %s", err)
}
err = chatdb.UpdateWith("john", 123456)
if err != nil {
t.Errorf("UpdateWith(): %s", err)
}
if _, ok := chatdb.Db["john"]; !ok {
t.Errorf("UpdateWith(): john is missing")
}
_, err = os.Stat(file + ".bak")
if err != nil {
t.Errorf("InitChatDB(): chatdb backup not created (error = %s)", err)
}
content, err := ioutil.ReadFile(file)
if err != nil {
t.Errorf("ioutil.ReadFile: %s", err)
}
assert.Equal(t, "john: 123456\n", string(content), "chatdb content")
}

15
go.mod

@ -0,0 +1,15 @@
module github.com/Telegram-Photo-Album-Bot
go 1.14
require (
github.com/Flaque/filet v0.0.0-20190209224823-fc4d33cfcf93
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/google/uuid v1.1.1
github.com/magiconair/properties v1.8.1
github.com/spf13/afero v1.1.2
github.com/spf13/viper v1.6.3
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
golang.org/x/text v0.3.2
gopkg.in/yaml.v2 v2.2.8
)

159
go.sum

@ -0,0 +1,159 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Flaque/filet v0.0.0-20190209224823-fc4d33cfcf93 h1:NnAUCP75PRm8yWE7+MZBIAR6PA9iwsBYEc6ZNYOy+AQ=
github.com/Flaque/filet v0.0.0-20190209224823-fc4d33cfcf93/go.mod h1:TK+jB3mBs+8ZMWhU5BqZKnZWJ1MrLo8etNVg51ueTBo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-telegram-bot-api/telegram-bot-api v1.0.0 h1:HXVtsZ+yINQeyyhPFAUU4yKmeN+iFhJ87jXZOC016gs=
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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs=
github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQDeX7m2XsSOlQEnM=
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo=
gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

554
main.go

@ -2,38 +2,30 @@ package main
import ( import (
"fmt" "fmt"
"io"
"io/ioutil"
"log" "log"
"net/http"
"os" "os"
"regexp" "path/filepath"
"strings"
"time" "time"
"unicode"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api"
"github.com/spf13/viper" "github.com/spf13/viper"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"gopkg.in/yaml.v2"
) )
var chatDB map[string]int64 = make(map[string]int64) func initConfig() {
// how many seconds to wait between retries, upon Telegram API errors
viper.SetDefault("RetryDelay", 60)
// max duration between two telegram updates
viper.SetDefault("TelegramNewUpdateTimeout", 60)
func main() { // Default messages
viper.SetDefault("MsgForbidden", "Access Denied") viper.SetDefault("MsgForbidden", "Access Denied")
viper.SetDefault("MsgHelp", "Hello") viper.SetDefault("MsgHelp", "Hello")
viper.SetDefault("MsgMissingAlbumName", "The album name is missing") viper.SetDefault("MsgMissingAlbumName", "The album name is missing")
viper.SetDefault("MsgAlbumAlreadyCreated", "An album has already been created")
viper.SetDefault("MsgServerError", "Server Error") viper.SetDefault("MsgServerError", "Server Error")
viper.SetDefault("MsgAlbumCreated", "Album created") viper.SetDefault("MsgAlbumCreated", "Album created")
viper.SetDefault("MsgNoAlbum", "No album is currently open")
viper.SetDefault("MsgAlbumClosed", "Album closed")
viper.SetDefault("MsgDoNotUnderstand", "Unknown command") viper.SetDefault("MsgDoNotUnderstand", "Unknown command")
viper.SetDefault("MsgInfoNoAlbum", "There is no album started, yet.")
viper.SetDefault("MsgNoUsername", "Sorry, you need to set your username") viper.SetDefault("MsgNoUsername", "Sorry, you need to set your username")
viper.SetDefault("MsgThankYouMedia", "Got it, thanks!") viper.SetDefault("MsgThankYouMedia", "Got it, thanks!")
viper.SetDefault("MsgThankYouText", "Thank you!")
viper.SetDefault("MsgSendMeSomething", "OK. Send me something.") viper.SetDefault("MsgSendMeSomething", "OK. Send me something.")
viper.SetConfigName("photo-bot") // name of config file (without extension) viper.SetConfigName("photo-bot") // name of config file (without extension)
@ -44,521 +36,91 @@ func main() {
if err != nil { if err != nil {
panic(fmt.Errorf("Cannot read config file: %s\n", err)) panic(fmt.Errorf("Cannot read config file: %s\n", err))
} }
}
func initLogFile() {
logFile := viper.GetString("LogFile") logFile := viper.GetString("LogFile")
var logHandle *os.File
if logFile != "" { if logFile != "" {
logHandle, err = os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) logHandle, err := os.OpenFile(logFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil { if err != nil {
panic(fmt.Errorf("Cannot open log file '%s': %s\n", logFile, err)) panic(fmt.Errorf("Cannot open log file '%s': %s\n", logFile, err))
} }
defer logHandle.Close()
log.SetOutput(logHandle) log.SetOutput(logHandle)
} }
}
target_dir := viper.GetString("TargetDir") func validateConfig() {
if target_dir == "" { targetDir := viper.GetString("TargetDir")
panic("No target directory provided!") if targetDir == "" {
log.Fatal("No target directory provided!")
} }
_, err = os.Stat(target_dir) _, err := os.Stat(targetDir)
if err != nil && os.IsNotExist(err) { if err != nil && os.IsNotExist(err) {
panic(fmt.Errorf("Cannot find target directory: %s: %s\n", target_dir, err)) log.Fatalf("Cannot find target directory: %s: %s", targetDir, err)
} }
authorized_users_list := viper.GetStringSlice("AuthorizedUsers") retryDelay := viper.GetInt("RetryDelay")
if len(authorized_users_list) == 0 { if retryDelay <= 0 {
panic(fmt.Errorf("A list of AuthorizedUsers must be given\n")) log.Fatal("The TelegramNewUpdateTimeout cannot be zero or negative!")
} }
authorized_users := map[string]bool{}
for _, item := range authorized_users_list { timeout := viper.GetInt("TelegramNewUpdateTimeout")
authorized_users[item] = true if timeout <= 0 {
log.Fatal("The TelegramNewUpdateTimeout cannot be zero or negative!")
} }
token := viper.GetString("TelegramToken") token := viper.GetString("TelegramToken")
if token == "" { if token == "" {
panic("No Telegram Bot Token provided!") log.Fatal("No Telegram Bot Token provided!")
} }
bot, err := tgbotapi.NewBotAPI(token) authorizedUsersList := viper.GetStringSlice("AuthorizedUsers")
if err != nil { if len(authorizedUsersList) == 0 {
log.Panic(err) log.Fatal("A list of AuthorizedUsers must be given!")
}
bot.Debug = viper.GetBool("TelegramDebug")
log.Printf("Authorized on account %s", bot.Self.UserName)
u := tgbotapi.NewUpdate(0)
u.Timeout = 60
updates, err := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil || update.Message.From == nil {
continue
}
text := update.Message.Text
username := update.Message.From.UserName
if username == "" {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgNoUsername"))
continue
}
if !authorized_users[username] {
log.Printf("[%s] unauthorized user", username)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgForbidden"))
continue
}
err := updateChatDB(update.Message)
if err != nil {
log.Printf("[%s] cannot update chat db: %s", username, err)
}
if text != "" {
if update.Message.IsCommand() {
log.Printf("[%s] command: %s", username, text)
switch update.Message.Command() {
case "start", "aide", "help":
replyWithMessage(bot, update.Message, viper.GetString("MsgHelp"))
case "nouvelAlbum":
if len(text) < 14 {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgMissingAlbumName"))
continue
}
albumName := update.Message.CommandArguments()
if albumAlreadyOpen() {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgAlbumAlreadyCreated"))
continue
}
err := newAlbum(update.Message, albumName)
if err != nil {
log.Printf("[%s] cannot create album '%s': %s", username, albumName, err)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError"))
continue
}
replyWithMessage(bot, update.Message, viper.GetString("MsgAlbumCreated"))
case "info":
if albumAlreadyOpen() {
albumName, err := getInfo()
if err != nil {
log.Printf("[%s] cannot close current album: %s", username, err)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError"))
continue
}
replyWithMessage(bot, update.Message, fmt.Sprintf(viper.GetString("MsgInfo"), albumName))
} else {
replyWithMessage(bot, update.Message, viper.GetString("MsgInfoNoAlbum"))
}
case "pourLouise":
if !albumAlreadyOpen() {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgNoAlbum"))
continue
}
replyWithForcedReply(bot, update.Message, viper.GetString("MsgSendMeSomething"))
case "cloreAlbum":
if !albumAlreadyOpen() {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgNoAlbum"))
continue
}
err := closeAlbum()
if err != nil {
log.Printf("[%s] cannot close current album: %s", username, err)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError"))
continue
}
replyWithMessage(bot, update.Message, viper.GetString("MsgAlbumClosed"))
default:
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgDoNotUnderstand"))
continue
}
} else {
if !albumAlreadyOpen() {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgNoAlbum"))
continue
}
err := addMessageToAlbum(update.Message)
if err != nil {
log.Printf("[%s] cannot add text '%s' to current album: %s", username, update.Message.Text, err)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError"))
continue
}
replyWithMessage(bot, update.Message, viper.GetString("MsgThankYouText"))
}
} else if update.Message.Photo != nil {
if !albumAlreadyOpen() {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgNoAlbum"))
continue
}
err := handlePhoto(bot, update.Message)
if err != nil {
log.Printf("[%s] cannot add photo to current album: %s", username, err)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError"))
continue
}
dispatchMessage(bot, update.Message)
replyWithMessage(bot, update.Message, viper.GetString("MsgThankYouMedia"))
} else if update.Message.Video != nil {
if !albumAlreadyOpen() {
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgNoAlbum"))
continue
}
err := handleVideo(bot, update.Message)
if err != nil {
log.Printf("[%s] cannot add video to current album: %s", username, err)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError"))
continue
}
dispatchMessage(bot, update.Message)
replyWithMessage(bot, update.Message, viper.GetString("MsgThankYouMedia"))
} else {
log.Printf("[%s] cannot handle this type of message", username)
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgDoNotUnderstand"))
continue
}
} }
} }
func updateChatDB(message *tgbotapi.Message) error { func main() {
target_dir := viper.GetString("TargetDir") initConfig()
if len(chatDB) == 0 { validateConfig()
yamlData, err := ioutil.ReadFile(target_dir + "/db/chatdb.yaml")
if err != nil {
log.Printf("cannot read chat db: %s", err)
} else {
err = yaml.Unmarshal(yamlData, &chatDB)
if err != nil {
log.Printf("cannot unmarshal chat db: %s", err)
}
}
}
if _, ok := chatDB[message.From.UserName]; !ok {
chatDB[message.From.UserName] = message.Chat.ID
yamlData, err := yaml.Marshal(chatDB)
if err != nil {
return err
}
os.MkdirAll(target_dir+"/db/", os.ModePerm)
err = ioutil.WriteFile(target_dir+"/db/chatdb.yaml", yamlData, 0644)
if err != nil {
return err
}
}
return nil
}
func replyWithForcedReply(bot *tgbotapi.BotAPI, message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ReplyMarkup = tgbotapi.ForceReply{
ForceReply: true,
Selective: true,
}
_, err := bot.Send(msg)
return err
}
func replyToCommandWithMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
msg.ReplyToMessageID = message.MessageID
_, err := bot.Send(msg)
return err
}
func replyWithMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message, text string) error {
msg := tgbotapi.NewMessage(message.Chat.ID, text)
_, err := bot.Send(msg)
return err
}
func dispatchMessage(bot *tgbotapi.BotAPI, message *tgbotapi.Message) {
users := viper.GetStringSlice("AuthorizedUsers")
for _, user := range users {
if user != message.From.UserName {
if _, ok := chatDB[user]; !ok {
log.Printf("[%s] The chat db does not have any mapping for %s, skipping...", message.From.UserName, user)
continue
}
msg := tgbotapi.NewForward(chatDB[user], message.Chat.ID, message.MessageID) // Create the Bot
photoBot := InitBot(viper.GetString("TargetDir"))
photoBot.Telegram.RetryDelay = time.Duration(viper.GetInt("RetryDelay")) * time.Second
photoBot.Telegram.NewUpdateTimeout = viper.GetInt("TelegramNewUpdateTimeout")
_, err := bot.Send(msg) // Fill the authorized users
if err != nil { for _, item := range viper.GetStringSlice("AuthorizedUsers") {
log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, chatDB[user]) photoBot.Telegram.AuthorizedUsers[item] = true
}
}
} }
}
func handlePhoto(bot *tgbotapi.BotAPI, message *tgbotapi.Message) error { targetDir := viper.GetString("TargetDir")
fileId := "" for _, dir := range []string{"data", "db"} {
maxWidth := 0 fullPath := filepath.Join(targetDir, dir)
for _, photo := range *message.Photo { var err error = os.MkdirAll(fullPath, 0777)
if photo.Width > maxWidth { if err != nil {
fileId = photo.FileID log.Fatalf("os.MkdirAll: %s: %s\n", fullPath, err)
maxWidth = photo.Width
} }
} }
photoFileName, err := getFile(bot, message, fileId) // Create the ChatDB and inject it
if err != nil { chatDB, err := InitChatDB(filepath.Join(targetDir, "db", "chatdb.yaml"))
return err
}
// parse the message timestamp
t := time.Unix(int64(message.Date), 0)
chat := [1]map[string]string{{
"type": "photo",
"date": t.Format("2006-01-02T15:04:05-0700"),
"from": "telegram",
"telegramFileId": fileId,
"username": message.From.UserName,
"firstname": message.From.FirstName,
"lastname": message.From.LastName,
"filename": photoFileName,
"message": message.Caption,
}}
yamlData, err := yaml.Marshal(chat)
if err != nil {
return err
}
target_dir := viper.GetString("TargetDir")
return appendToFile(target_dir+"/data/.current/chat.yaml", yamlData)
}
func handleVideo(bot *tgbotapi.BotAPI, message *tgbotapi.Message) error {
videoFileName, err := getFile(bot, message, message.Video.FileID)
if err != nil {
return err
}
thumbFileName, err := getFile(bot, message, message.Video.Thumbnail.FileID)
if err != nil { if err != nil {
log.Printf("[%s] Cannot download video thumbnail: %s", message.From.UserName, err) panic(err)
} }
photoBot.Telegram.ChatDB = chatDB
// parse the message timestamp // Create the MediaStore and inject it
t := time.Unix(int64(message.Date), 0) mediaStore, err := InitMediaStore(filepath.Join(targetDir, "data"))
chat := [1]map[string]string{{
"type": "video",
"date": t.Format("2006-01-02T15:04:05-0700"),
"from": "telegram",
"telegramFileId": message.Video.FileID,
"username": message.From.UserName,
"firstname": message.From.FirstName,
"lastname": message.From.LastName,
"filename": videoFileName,
"thumb_filename": thumbFileName,
"message": message.Caption,
}}
yamlData, err := yaml.Marshal(chat)
if err != nil { if err != nil {
return err panic(err)
} }
photoBot.MediaStore = mediaStore
target_dir := viper.GetString("TargetDir") // Start the bot
return appendToFile(target_dir+"/data/.current/chat.yaml", yamlData) photoBot.StartBot(viper.GetString("TelegramToken"))
} photoBot.Telegram.API.Debug = viper.GetBool("TelegramDebug")
func getFile(bot *tgbotapi.BotAPI, message *tgbotapi.Message, fileId string) (string, error) {
url, err := bot.GetFileDirectURL(fileId)
if err != nil {
return "", err
}
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
// Only the first 512 bytes are used to sniff the content type.
buffer := make([]byte, 512)
n, err := resp.Body.Read(buffer)
if err != nil {
return "", err
}
// Detect the content-type
contentType := http.DetectContentType(buffer)
var extension string
if contentType == "image/jpeg" {
extension = ".jpeg"
} else if contentType == "video/mp4" {
extension = ".mp4"
} else {
log.Printf("[%s] Unknown media content-type '%s'", message.From.UserName, contentType)
extension = ".bin"
}
// Create the file
target_dir := viper.GetString("TargetDir")
filename := target_dir + "/data/.current/" + fileId + extension
out, err := os.Create(filename)
if err != nil {
return "", err
}
defer out.Close()
// Write back the first 512 bytes
n, err = out.Write(buffer[0:n])
if err != nil {
return "", err
}
// Write the rest of the body to file
_, err = io.Copy(out, resp.Body)
if err != nil {
return "", err
}
return fileId + extension, nil
}
func closeAlbum() error {
target_dir := viper.GetString("TargetDir")
yamlData, err := ioutil.ReadFile(target_dir + "/data/.current/meta.yaml")
if err != nil {
return err
}
var metadata map[string]string = make(map[string]string)
err = yaml.UnmarshalStrict(yamlData, &metadata)
if err != nil {
return err
}
date, err := time.Parse("2006-01-02T15:04:05-0700", metadata["date"])
if err != nil {
return err
}
folderName := date.Format("2006-01-02") + "-" + sanitizeAlbumName(metadata["title"])
err = os.Rename(target_dir+"/data/.current/", target_dir+"/data/"+folderName)
if err != nil {
return err
}
return nil
}
func getInfo() (string, error) {
target_dir := viper.GetString("TargetDir")
yamlData, err := ioutil.ReadFile(target_dir + "/data/.current/meta.yaml")
if err != nil {
return "", err
}
var metadata map[string]string = make(map[string]string)
err = yaml.UnmarshalStrict(yamlData, &metadata)
if err != nil {
return "", err
}
return metadata["title"], nil
}
func albumAlreadyOpen() bool {
target_dir := viper.GetString("TargetDir")
_, err := os.Stat(target_dir + "/data/.current")
return err == nil
}
func newAlbum(message *tgbotapi.Message, albumName string) error {
target_dir := viper.GetString("TargetDir")
os.MkdirAll(target_dir+"/data/.current", os.ModePerm)
metadata := map[string]string{
"title": albumName,
"from": "telegram",
"username": message.From.UserName,
"firstname": message.From.FirstName,
"lastname": message.From.LastName,
"date": time.Now().Format("2006-01-02T15:04:05-0700"),
}
yamlData, err := yaml.Marshal(metadata)
if err != nil {
return err
}
err = ioutil.WriteFile(target_dir+"/data/.current/meta.yaml", yamlData, 0644)
if err != nil {
return err
}
return nil
}
func addMessageToAlbum(message *tgbotapi.Message) error {
target_dir := viper.GetString("TargetDir")
// parse the message timestamp
t := time.Unix(int64(message.Date), 0)
chat := [1]map[string]string{{
"type": "text",
"date": t.Format("2006-01-02T15:04:05-0700"),
"from": "telegram",
"username": message.From.UserName,
"firstname": message.From.FirstName,
"lastname": message.From.LastName,
"message": message.Text,
}}
yamlData, err := yaml.Marshal(chat)
if err != nil {
return err
}
return appendToFile(target_dir+"/data/.current/chat.yaml", yamlData)
}
func appendToFile(filename string, data []byte) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer f.Close()
if _, err = f.Write(data); err != nil {
return err
}
return nil
}
func sanitizeAlbumName(albumName string) string {
albumName = strings.ToLower(albumName)
t := transform.Chain(norm.NFD, transform.RemoveFunc(func(r rune) bool {
return unicode.Is(unicode.Mn, r)
}), norm.NFC)
albumName, _, _ = transform.String(t, albumName)
reg, err := regexp.Compile("\\s+")
if err != nil {
panic(fmt.Errorf("Cannot compile regex: %s", err))
}
albumName = reg.ReplaceAllString(albumName, "-")
reg, err = regexp.Compile("[^-a-zA-Z0-9_]+")
if err != nil {
panic(fmt.Errorf("Cannot compile regex: %s", err))
}
albumName = reg.ReplaceAllString(albumName, "")
return albumName initLogFile()
photoBot.Process()
} }

180
media_store.go

@ -0,0 +1,180 @@
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"time"
"unicode"
"github.com/google/uuid"
"golang.org/x/text/transform"
"golang.org/x/text/unicode/norm"
"gopkg.in/yaml.v2"
)
type MediaStore struct {
StoreLocation string
}
func InitMediaStore(storeLocation string) (*MediaStore, error) {
err := os.MkdirAll(filepath.Join(storeLocation, ".current"), os.ModePerm)
if err != nil {
return nil, err
}
return &MediaStore{StoreLocation: storeLocation}, nil
}
func (store *MediaStore) GetUniqueID() string {
return uuid.New().String()
}
func (store *MediaStore) AddFile(fileName string) (*os.File, error) {
filename := filepath.Join(store.StoreLocation, ".current", fileName)
return os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0666)
}
func (store *MediaStore) CommitPhoto(id string, timestamp time.Time, caption string) error {
return store.commitMedia(id, timestamp, caption, "photo")
}
func (store *MediaStore) CommitVideo(id string, timestamp time.Time, caption string) error {
return store.commitMedia(id, timestamp, caption, "video")
}
func (store *MediaStore) commitMedia(id string, timestamp time.Time, caption string, mediaType string) error {
entry := [1]map[string]string{{
"type": mediaType,
"date": timestamp.Format("2006-01-02T15:04:05-0700"),
"caption": caption,
"id": id,
}}
yamlData, err := yaml.Marshal(entry)
if err != nil {
return err
}
return appendToFile(filepath.Join(store.StoreLocation, ".current", "chat.yaml"), yamlData)
}
func appendToFile(filename string, data []byte) error {
f, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
if err != nil {
return err
}
defer f.Close()
if _, err = f.Write(data); err != nil {
return err
}
return nil
}
func (store *MediaStore) GetCurrentAlbum() (string, error) {
yamlData, err := ioutil.ReadFile(filepath.Join(store.StoreLocation, ".current", "meta.yaml"))
if err != nil {
if os.IsNotExist(err) {
// the album has not yet a name, it is not an error
return "", nil
} else {
return "", err
}
}
var metadata map[string]string = make(map[string]string)
err = yaml.UnmarshalStrict(yamlData, &metadata)
if err != nil {
return "", err
}
return metadata["title"], nil
}
func (store *MediaStore) CloseAlbum() error {
yamlData, err := ioutil.ReadFile(filepath.Join(store.StoreLocation, ".current", "meta.yaml"))
if err != nil {
return err
}
var metadata map[string]string = make(map[string]string)
err = yaml.UnmarshalStrict(yamlData, &metadata)
if err != nil {
return err
}
date, err := time.Parse("2006-01-02T15:04:05-0700", metadata["date"])
if err != nil {
return err
}
folderName := date.Format("2006-01-02") + "-" + sanitizeAlbumName(metadata["title"])
err = os.Rename(filepath.Join(store.StoreLocation, ".current"), filepath.Join(store.StoreLocation, folderName))
if err != nil {
return err
}
return nil
}
func fileExists(filename string) bool {
_, err := os.Stat(filename)
return err == nil
}
func (store *MediaStore) NewAlbum(title string) error {
if fileExists(filepath.Join(store.StoreLocation, ".current/")) {
if fileExists(filepath.Join(store.StoreLocation, "/.current/meta.yaml")) {
err := store.CloseAlbum()
if err != nil {
return err
}
}
}
err := os.MkdirAll(filepath.Join(store.StoreLocation, ".current/"), os.ModePerm)
if err != nil {
return err
}
metadata := map[string]string{
"title": title,
"date": time.Now().Format("2006-01-02T15:04:05-0700"),
}
yamlData, err := yaml.Marshal(metadata)
if err != nil {
return err
}
err = ioutil.WriteFile(filepath.Join(store.StoreLocation, ".current", "meta.yaml"), yamlData, 0644)
if err != nil {
return err
}
return nil
}
func sanitizeAlbumName(albumName string) string {
albumName = strings.ToLower(albumName)
t := transform.Chain(norm.NFD, transform.RemoveFunc(func(r rune) bool {
return unicode.Is(unicode.Mn, r)
}), norm.NFC)
albumName, _, _ = transform.String(t, albumName)
reg, err := regexp.Compile("\\s+")
if err != nil {
panic(fmt.Errorf("Cannot compile regex: %s", err))
}
albumName = reg.ReplaceAllString(albumName, "-")
reg, err = regexp.Compile("[^-a-zA-Z0-9_]+")
if err != nil {
panic(fmt.Errorf("Cannot compile regex: %s", err))
}
albumName = reg.ReplaceAllString(albumName, "")
return albumName
}

29
media_store_test.go

@ -0,0 +1,29 @@
package main
import (
"os"
"path/filepath"
"testing"
)
func TestSanitizeAlbumName(t *testing.T) {
input := "Mes premières années"
want := "mes-premieres-annees"
if got := sanitizeAlbumName(input); got != want {
t.Errorf("sanitizeAlbumName() = %q, want %q", got, want)
}
}
func TestNewMediaStore(t *testing.T) {
tmp := createTempDir(t)
defer tmp.cleanup(t)
_, err := InitMediaStore(tmp.RootDir)
if err != nil {
t.Errorf("InitMediaStore(): error %s", err)
}
stat, err := os.Stat(filepath.Join(tmp.RootDir, ".current"))
if err != nil || !stat.IsDir() {
t.Errorf("InitMediaStore(): .current not created (error = %s)", err)
}
}

24
test.go

@ -0,0 +1,24 @@
package main
import (
"io/ioutil"
"os"
"testing"
)
type TestCaseTempFile struct {
RootDir string
}
func createTempDir(t *testing.T) TestCaseTempFile {
tmp := os.TempDir()
dir, err := ioutil.TempDir(tmp, "unittest-*")
if err != nil {
t.Errorf("newTmpDir: ioutil.TempDir: %s", err)
}
return TestCaseTempFile{RootDir: dir}
}
func (tmp *TestCaseTempFile) cleanup(t *testing.T) {
//os.RemoveAll(tmp.RootDir)
}
Loading…
Cancel
Save