commit
6c195e677b
3 changed files with 518 additions and 0 deletions
@ -0,0 +1 @@ |
|||||
|
*.yaml |
||||
@ -0,0 +1,36 @@ |
|||||
|
# The Photo-Album Bot for Telegram |
||||
|
|
||||
|
## Compilation |
||||
|
|
||||
|
``` |
||||
|
go get -u github.com/go-telegram-bot-api/telegram-bot-api |
||||
|
go get -u github.com/spf13/viper |
||||
|
go get -u gopkg.in/yaml.v2 |
||||
|
``` |
||||
|
|
||||
|
## Create a Bot |
||||
|
|
||||
|
Talk to [BotFather](https://core.telegram.org/bots#6-botfather) to create your bot. |
||||
|
|
||||
|
``` |
||||
|
/newbot |
||||
|
``` |
||||
|
|
||||
|
Keep your bot token secure and safe! |
||||
|
|
||||
|
## Create the configuration file |
||||
|
|
||||
|
Create a file named `photo-bot.yaml` in the current directory. |
||||
|
|
||||
|
```yaml |
||||
|
TelegramToken: "bot.token.here" |
||||
|
TelegramDebug: true |
||||
|
TargetDir: /srv/photos |
||||
|
AuthorizedUsers: |
||||
|
- john |
||||
|
- jane |
||||
|
``` |
||||
|
|
||||
|
## Documentation |
||||
|
|
||||
|
- https://core.telegram.org/bots/api |
||||
@ -0,0 +1,481 @@ |
|||||
|
package main |
||||
|
|
||||
|
import ( |
||||
|
"log" |
||||
|
"os" |
||||
|
"io" |
||||
|
"net/http" |
||||
|
"fmt" |
||||
|
"strings" |
||||
|
"time" |
||||
|
"io/ioutil" |
||||
|
"regexp" |
||||
|
"github.com/go-telegram-bot-api/telegram-bot-api" |
||||
|
"github.com/spf13/viper" |
||||
|
"unicode" |
||||
|
"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 main() { |
||||
|
viper.SetDefault("MsgForbidden", "Access Denied") |
||||
|
viper.SetDefault("MsgHelp", "Hello") |
||||
|
viper.SetDefault("MsgMissingAlbumName", "The album name is missing") |
||||
|
viper.SetDefault("MsgAlbumAlreadyCreated", "An album has already been created") |
||||
|
viper.SetDefault("MsgServerError", "Server Error") |
||||
|
viper.SetDefault("MsgAlbumCreated", "Album created") |
||||
|
viper.SetDefault("MsgNoAlbum", "No album is currently open") |
||||
|
viper.SetDefault("MsgAlbumClosed", "Album closed") |
||||
|
viper.SetDefault("MsgDoNotUnderstand", "Unknown command") |
||||
|
|
||||
|
viper.SetConfigName("photo-bot") // name of config file (without extension)
|
||||
|
viper.AddConfigPath("/etc/photo-bot/") |
||||
|
viper.AddConfigPath("$HOME/.photo-bot") |
||||
|
viper.AddConfigPath(".") // optionally look for config in the working directory
|
||||
|
err := viper.ReadInConfig() |
||||
|
if err != nil { |
||||
|
panic(fmt.Errorf("Cannot read config file: %s\n", err)) |
||||
|
} |
||||
|
|
||||
|
target_dir := viper.GetString("TargetDir") |
||||
|
if (target_dir == "") { |
||||
|
panic("No target directory provided!") |
||||
|
} |
||||
|
_, err = os.Stat(target_dir) |
||||
|
if err != nil && os.IsNotExist(err) { |
||||
|
panic(fmt.Errorf("Cannot find target directory: %s: %s\n", target_dir, err)) |
||||
|
} |
||||
|
|
||||
|
authorized_users_list := viper.GetStringSlice("AuthorizedUsers") |
||||
|
if len(authorized_users_list) == 0 { |
||||
|
panic(fmt.Errorf("A list of AuthorizedUsers must be given\n")) |
||||
|
} |
||||
|
authorized_users := map[string]bool{} |
||||
|
for _, item := range authorized_users_list { |
||||
|
authorized_users[item] = true |
||||
|
} |
||||
|
|
||||
|
token := viper.GetString("TelegramToken") |
||||
|
if (token == "") { |
||||
|
panic("No Telegram Bot Token provided!") |
||||
|
} |
||||
|
|
||||
|
bot, err := tgbotapi.NewBotAPI(token) |
||||
|
if err != nil { |
||||
|
log.Panic(err) |
||||
|
} |
||||
|
|
||||
|
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 == "") { |
||||
|
continue |
||||
|
} |
||||
|
if (! authorized_users[username]) { |
||||
|
log.Printf("[%s] unauthorized user", username) |
||||
|
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgForbidden")) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
updateChatDB(update.Message) |
||||
|
|
||||
|
if text != "" { |
||||
|
if strings.HasPrefix(text, "/") { |
||||
|
log.Printf("[%s] command: %s", username, text) |
||||
|
if strings.HasPrefix(text, "/start") || strings.HasPrefix(text, "/aide") || strings.HasPrefix(text, "/help") { |
||||
|
replyWithMessage(bot, update.Message, viper.GetString("MsgHelp")) |
||||
|
} else if strings.HasPrefix(text, "/nouvelAlbum") { |
||||
|
if len(text) < 14 { |
||||
|
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgMissingAlbumName")) |
||||
|
continue |
||||
|
} |
||||
|
albumName := text[13:len(text)] |
||||
|
|
||||
|
if albumAlreadyOpen() { |
||||
|
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgAlbumAlreadyCreated")) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
err := newAlbum(username, albumName) |
||||
|
if err != nil { |
||||
|
log.Printf("[%s] cannot create album '%s': %s", username, albumName, err) |
||||
|
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgServerError")) |
||||
|
continue |
||||
|
} |
||||
|
|
||||
|
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgAlbumCreated")) |
||||
|
} else if strings.HasPrefix(text, "/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 |
||||
|
} |
||||
|
|
||||
|
replyToCommandWithMessage(bot, update.Message, viper.GetString("MsgAlbumClosed")) |
||||
|
} else { |
||||
|
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 |
||||
|
} |
||||
|
//dispatchMessage(bot, update.Message)
|
||||
|
} |
||||
|
} 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) |
||||
|
} 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) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func updateChatDB(message *tgbotapi.Message) error { |
||||
|
target_dir := viper.GetString("TargetDir") |
||||
|
if len(chatDB) == 0 { |
||||
|
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 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[message.From.UserName]; !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) |
||||
|
|
||||
|
_, err := bot.Send(msg) |
||||
|
if err != nil { |
||||
|
log.Printf("[%s] Cannot dispatch message to %s (chat id = %d)", message.From.UserName, user, chatDB[user]) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
func handlePhoto(bot *tgbotapi.BotAPI, message *tgbotapi.Message) error { |
||||
|
fileId := "" |
||||
|
maxWidth := 0 |
||||
|
for _, photo := range (*message.Photo) { |
||||
|
if photo.Width > maxWidth { |
||||
|
fileId = photo.FileID |
||||
|
maxWidth = photo.Width |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
photoFileName, err := getFile(bot, message, fileId) |
||||
|
if err != nil { |
||||
|
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"), |
||||
|
"username": message.From.UserName, |
||||
|
"filename": photoFileName, |
||||
|
}} |
||||
|
|
||||
|
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 { |
||||
|
log.Printf("[%s] Cannot download video thumbnail: %s", message.From.UserName, err) |
||||
|
} |
||||
|
|
||||
|
// parse the message timestamp
|
||||
|
t := time.Unix(int64(message.Date), 0) |
||||
|
chat := [1]map[string]string{{ |
||||
|
"type": "video", |
||||
|
"date": t.Format("2006-01-02T15:04:05-0700"), |
||||
|
"username": message.From.UserName, |
||||
|
"filename": videoFileName, |
||||
|
"thumb_filename": thumbFileName, |
||||
|
}} |
||||
|
|
||||
|
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 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) |
||||
|
extension := ".bin" |
||||
|
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) |
||||
|
} |
||||
|
|
||||
|
// 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 |
||||
|
} |
||||
|
|
||||
|
type AlbumMetadata struct { |
||||
|
Title string `yaml:"title"` |
||||
|
Date string `yaml:"date"` |
||||
|
Folder string `yaml:"folder"` |
||||
|
} |
||||
|
|
||||
|
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 AlbumMetadata |
||||
|
err = yaml.UnmarshalStrict(yamlData, &metadata) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
err = os.Rename(target_dir + "/data/.current/", target_dir + "/data/" + metadata.Folder) |
||||
|
if err != nil { |
||||
|
return err |
||||
|
} |
||||
|
|
||||
|
return nil |
||||
|
} |
||||
|
|
||||
|
func albumAlreadyOpen() bool { |
||||
|
target_dir := viper.GetString("TargetDir") |
||||
|
_, err := os.Stat(target_dir + "/data/.current") |
||||
|
return err == nil |
||||
|
} |
||||
|
|
||||
|
func newAlbum(username string, albumName string) error { |
||||
|
target_dir := viper.GetString("TargetDir") |
||||
|
os.MkdirAll(target_dir + "/data/.current", os.ModePerm) |
||||
|
|
||||
|
metadata := map[string]string{ |
||||
|
"title": albumName, |
||||
|
"date": time.Now().Format("2006-01-02T15:04:05-0700"), |
||||
|
"folder": fmt.Sprintf("%s-%s", time.Now().Format("2006-01-02"), sanitizeAlbumName(albumName)), |
||||
|
} |
||||
|
|
||||
|
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 |
||||
|
} |
||||
|
|
||||
|
err = ioutil.WriteFile(target_dir + "/data/.current/chat.yaml", []byte{}, 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"), |
||||
|
"username": message.From.UserName, |
||||
|
"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_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 |
||||
|
} |
||||
Loading…
Reference in new issue