From 18d3faba628d420a0434c2b0cb1cf3d0fa23188e Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 7 Nov 2023 00:17:40 +0200 Subject: [PATCH 1/7] Initial telegram interface implementation --- README.md | 10 ++ fs/fs.go | 3 + fs/telegram/example.conf | 19 +++ fs/telegram/telegram.go | 295 +++++++++++++++++++++++++++++++++++++++ go.mod | 1 + go.sum | 2 + 6 files changed, 330 insertions(+) create mode 100644 fs/telegram/example.conf create mode 100644 fs/telegram/telegram.go diff --git a/README.md b/README.md index a3d88299..50c33746 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ At the current stage, supported backend are: - [Google Drive](https://developers.google.com/drive) (see [doc](https://github.com/fclairamb/ftpserver/tree/master/fs/gdrive)) through [afero-gdrive](https://github.com/fclairamb/afero-gdrive) - [SFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) through [afero's sftpfs](https://github.com/spf13/afero/) - Email through [go-mail](https://github.com/go-mail/mail) thanks to [@x-way](https://github.com/x-way) +- Telegram through [telebot](https://github.com/tucnak/telebot) by [@slayer](https://github.com/slayer) And with those are supported common parameters to switch them to read-only, enable login access, or use a temporary directory file (see [doc](https://github.com/fclairamb/ftpserver/tree/master/fs)). @@ -191,6 +192,15 @@ Here is a sample config file: "password": "password", "hostname": "192.168.168.11:22" } + }, + { + "user": "telegram", + "pass": "telegram", + "fs": "telegram", + "params": { + "Token": "OBTAIN_TOKEN_FROM_BOTFATHER", + "ChatID": "INSERT_CHAT_ID_HERE-INVITE_BOT_TO_CHAT_MANUALLY" + } } ] } diff --git a/fs/fs.go b/fs/fs.go index aa01826e..24ea4bfc 100644 --- a/fs/fs.go +++ b/fs/fs.go @@ -15,6 +15,7 @@ import ( "github.com/fclairamb/ftpserver/fs/mail" "github.com/fclairamb/ftpserver/fs/s3" "github.com/fclairamb/ftpserver/fs/sftp" + "github.com/fclairamb/ftpserver/fs/telegram" ) // UnsupportedFsError is returned when the described file system is not supported @@ -45,6 +46,8 @@ func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) { fs, err = gdrive.LoadFs(access, logger.With("component", "gdrive")) case "dropbox": fs, err = dropbox.LoadFs(access) + case "telegram": + fs, err = telegram.LoadFs(access, logger.With("component", "telegram")) default: fs, err = nil, &UnsupportedFsError{Type: access.Fs} } diff --git a/fs/telegram/example.conf b/fs/telegram/example.conf new file mode 100644 index 00000000..99524f57 --- /dev/null +++ b/fs/telegram/example.conf @@ -0,0 +1,19 @@ +{ + "version": 1, + "accesses": [ + { + "user": "test", + "pass": "test", + "fs": "telegram", + "params": { + "Token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789", + "ChatID": "123456789" + } + + } + ], + "passive_transfer_port_range": { + "start": 2122, + "end": 2130 + } +} diff --git a/fs/telegram/telegram.go b/fs/telegram/telegram.go new file mode 100644 index 00000000..24629759 --- /dev/null +++ b/fs/telegram/telegram.go @@ -0,0 +1,295 @@ +// Package telegram provides a telegram access layer +package telegram + +import ( + "errors" + "fmt" + "io" + "path/filepath" + "strconv" + "strings" + + "os" + "sync/atomic" + "time" + + log "github.com/fclairamb/go-log" + tele "gopkg.in/telebot.v3" + + "github.com/spf13/afero" + + "github.com/fclairamb/ftpserver/config/confpar" + "gopkg.in/telebot.v3/middleware" +) + +// ErrNotImplemented is returned when something is not implemented +var ErrNotImplemented = errors.New("not implemented") + +// ErrNotFound is returned when something is not found +var ErrNotFound = errors.New("not found") + +// ErrInvalidParameter is returned when a parameter is invalid +var ErrInvalidParameter = errors.New("invalid parameter") + +// Fs is a write-only afero.Fs implementation using telegram as backend +type Fs struct { + Bot *tele.Bot + ChatID int64 + Logger log.Logger +} + +// File is the afero.File implementation +type File struct { + Path string + Content []byte + Fs *Fs + At int64 +} + +var imageExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif"} +var videoExtensions = []string{".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".mpeg", ".mpg", ".m4v", ".3gp", ".3g2"} +var textExtensions = []string{".txt", ".md"} +var audioExtensions = []string{".mp3", ".ogg", ".flac", ".wav", ".m4a", ".opus"} + +// LoadFs loads a file system from an access description +func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) { + + token := access.Params["Token"] + if token == "" { + return nil, fmt.Errorf("parameter Token is empty") + } + + chatID, err := strconv.ParseInt(access.Params["ChatID"], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid ChatID parameter: %v", err) + } + + pref := tele.Settings{ + Token: token, + Poller: &tele.LongPoller{Timeout: 10 * time.Second}, + } + + bot, err := tele.NewBot(pref) + if err != nil { + logger.Error("telegram bot initialization", "err", err) + return nil, err + } + bot.Use(middleware.Logger()) + bot.Use(middleware.AutoRespond()) + + // Just to check that the bot is working + bot.Handle("/hello", func(c tele.Context) error { + return c.Send("Hello!") + }) + + go func() { + // Run bot in the background + bot.Start() + }() + + fs := &Fs{ + Bot: bot, + Logger: logger, + ChatID: chatID, + } + + return fs, nil +} + +// Name of the file +func (f *File) Name() string { return f.Path } + +// Close closes the file transfer and does the actual transfer to telegram +func (f *File) Close() error { + if f.Fs == nil { + return ErrNotFound + } + + chat := tele.Chat{ID: f.Fs.ChatID} + var err error + basePath := filepath.Base(f.Path) + + if isExtension(f.Path, imageExtensions) { + photo := tele.Photo{File: tele.FromReader(f), Caption: basePath} + _, err = f.Fs.Bot.Send(&chat, &photo) + } else if isExtension(f.Path, videoExtensions) { + video := tele.Video{File: tele.FromReader(f), Caption: basePath} + _, err = f.Fs.Bot.Send(&chat, &video) + } else if isExtension(f.Path, audioExtensions) { + audio := tele.Audio{File: tele.FromReader(f), Caption: basePath} + _, err = f.Fs.Bot.Send(&chat, &audio) + } else if isExtension(f.Path, textExtensions) && len(f.Content) < 4096 { + if isExtension(f.Path, []string{".md"}) { + _, err = f.Fs.Bot.Send(&chat, string(f.Content), tele.ModeMarkdown) + } else { + _, err = f.Fs.Bot.Send(&chat, string(f.Content)) + } + } else { + document := tele.Document{File: tele.FromReader(f), Caption: basePath} + document.FileName = basePath + document.FileLocal = basePath + _, err = f.Fs.Bot.Send(&chat, &document) + } + + if err != nil { + f.Fs.Logger.Error("telegram Bot.Send()", "err", err) + return err + } + + f.Content = []byte{} + f.At = 0 + + return nil +} + +// Read stores the received file content into the local buffer +func (f *File) Read(b []byte) (int, error) { + n := 0 + + if len(b) > 0 && int(f.At) == len(f.Content) { + return 0, io.EOF + } + + if len(f.Content)-int(f.At) >= len(b) { + n = len(b) + } else { + n = len(f.Content) - int(f.At) + } + + copy(b, f.Content[f.At:f.At+int64(n)]) + atomic.AddInt64(&f.At, int64(n)) + + return n, nil +} + +// ReadAt is not implemented +func (f *File) ReadAt(_ []byte, _ int64) (int, error) { + return 0, ErrNotImplemented +} + +// Truncate is not implemented +func (f *File) Truncate(_ int64) error { + return nil +} + +// Readdir is not implemented +func (f *File) Readdir(_ int) ([]os.FileInfo, error) { + return []os.FileInfo{}, nil +} + +// Readdirnames is not implemented +func (f *File) Readdirnames(_ int) ([]string, error) { + return []string{}, nil +} + +// Seek is not implemented +func (f *File) Seek(_ int64, _ int) (int64, error) { + return 0, nil +} + +// Stat is not implemented +func (f *File) Stat() (os.FileInfo, error) { + f.Fs.Logger.Error("telegram File.Stat() not implemented") + return nil, ErrNotImplemented +} + +// Sync is not implemented +func (f *File) Sync() error { + return nil +} + +// WriteString is not implemented +func (f *File) WriteString(s string) (int, error) { + return 0, ErrNotImplemented +} + +// WriteAt is not implemented +func (f *File) WriteAt(b []byte, off int64) (int, error) { + return 0, ErrNotImplemented +} + +func (f *File) Write(b []byte) (int, error) { + f.Content = append(f.Content, b...) + + return len(b), nil +} + +// Name of the filesystem +func (m *Fs) Name() string { + return "telegram" +} + +// Chtimes is not implemented +func (m *Fs) Chtimes(name string, atime, mtime time.Time) error { + return nil +} + +// Chmod is not implemented +func (m *Fs) Chmod(name string, mode os.FileMode) error { + return nil +} + +// Rename is not implemented +func (m *Fs) Rename(name string, newname string) error { + return nil +} + +// Chown is not implemented +func (m *Fs) Chown(string, int, int) error { + return nil +} + +// RemoveAll is not implemented +func (m *Fs) RemoveAll(name string) error { + return nil +} + +// Remove is not implemented +func (m *Fs) Remove(name string) error { + return nil +} + +// Mkdir is not implemented +func (m *Fs) Mkdir(name string, mode os.FileMode) error { + return nil +} + +// MkdirAll is not implemented +func (m *Fs) MkdirAll(name string, mode os.FileMode) error { + return nil +} + +// Open opens a file buffer +func (m *Fs) Open(name string) (afero.File, error) { + return &File{Path: name, Fs: m}, nil +} + +// Create creates a file buffer +func (m *Fs) Create(name string) (afero.File, error) { + return &File{Path: name, Fs: m}, nil +} + +// OpenFile opens a file buffer +func (m *Fs) OpenFile(name string, flag int, mode os.FileMode) (afero.File, error) { + return &File{Path: name, Fs: m}, nil +} + +// Stat is not implemented +func (m *Fs) Stat(name string) (os.FileInfo, error) { + return nil, &os.PathError{Op: "stat", Path: name, Err: nil} +} + +// LstatIfPossible is not implemented +func (m *Fs) LstatIfPossible(name string) (os.FileInfo, bool, error) { + return nil, false, &os.PathError{Op: "lstat", Path: name, Err: nil} +} + +func isExtension(filename string, extensions []string) bool { + extension := strings.ToLower(filepath.Ext(filename)) + for _, ext := range extensions { + if extension == ext { + return true + } + } + return false +} diff --git a/go.mod b/go.mod index ee98afa3..2ca86aa8 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/tidwall/sjson v1.2.5 golang.org/x/crypto v0.14.0 golang.org/x/oauth2 v0.13.0 + gopkg.in/telebot.v3 v3.1.4 ) require ( diff --git a/go.sum b/go.sum index 30db08c3..7b38d442 100644 --- a/go.sum +++ b/go.sum @@ -1767,6 +1767,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw= +gopkg.in/telebot.v3 v3.1.4 h1:RMmN6FMKvh+j4sylImulZL1sgi7vMnCyD6NUsowkjK8= +gopkg.in/telebot.v3 v3.1.4/go.mod h1:GJKwwWqp9nSkIVN51eRKU78aB5f5OnQuWdwiIZfPbko= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 5406212644abfdb402dbe545c99fcfdb4d8eedf4 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 7 Nov 2023 00:22:47 +0200 Subject: [PATCH 2/7] bump go version to 1.21.3 --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1e906fa4..0a67e00f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: build: # The type of runner that the job will run on runs-on: ubuntu-22.04 - + # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE @@ -29,7 +29,7 @@ jobs: - name: Setup go uses: actions/setup-go@v4.1.0 with: - go-version: 1.19 + go-version: 1.21.3 - name: Build run: go build -v ./... From a631eba8c6d06056a9b2330c57266c89dfbe48c2 Mon Sep 17 00:00:00 2001 From: Vlad Date: Tue, 21 Nov 2023 23:18:23 +0200 Subject: [PATCH 3/7] telegram fakeFs implementation for clients which do Stat() --- README.md | 7 +-- fs/telegram/README.md | 43 +++++++++++++++++ fs/telegram/example.conf | 3 +- fs/telegram/fake_fs.go | 79 ++++++++++++++++++++++++++++++++ fs/telegram/file_info.go | 37 +++++++++++++++ fs/telegram/telegram.go | 99 ++++++++++++++++++++++++++++++++++------ 6 files changed, 249 insertions(+), 19 deletions(-) create mode 100644 fs/telegram/README.md create mode 100644 fs/telegram/fake_fs.go create mode 100644 fs/telegram/file_info.go diff --git a/README.md b/README.md index 50c33746..bc0a32b5 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ At the current stage, supported backend are: - [Google Drive](https://developers.google.com/drive) (see [doc](https://github.com/fclairamb/ftpserver/tree/master/fs/gdrive)) through [afero-gdrive](https://github.com/fclairamb/afero-gdrive) - [SFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) through [afero's sftpfs](https://github.com/spf13/afero/) - Email through [go-mail](https://github.com/go-mail/mail) thanks to [@x-way](https://github.com/x-way) -- Telegram through [telebot](https://github.com/tucnak/telebot) by [@slayer](https://github.com/slayer) +- Telegram through [telebot](https://github.com/tucnak/telebot) by [@slayer](https://github.com/slayer), see [doc](fs/telegram/README.md) And with those are supported common parameters to switch them to read-only, enable login access, or use a temporary directory file (see [doc](https://github.com/fclairamb/ftpserver/tree/master/fs)). @@ -197,9 +197,10 @@ Here is a sample config file: "user": "telegram", "pass": "telegram", "fs": "telegram", + "shared": true, "params": { - "Token": "OBTAIN_TOKEN_FROM_BOTFATHER", - "ChatID": "INSERT_CHAT_ID_HERE-INVITE_BOT_TO_CHAT_MANUALLY" + "Token": "", + "ChatID": "" } } ] diff --git a/fs/telegram/README.md b/fs/telegram/README.md new file mode 100644 index 00000000..8750d89b --- /dev/null +++ b/fs/telegram/README.md @@ -0,0 +1,43 @@ +# FTPServer Telegram connector + +## Register bot + +Read about telegram bots at https://core.telegram.org/bots/tutorial + +### Quick start + +- Create a bot with [@BotFather](https://t.me/BotFather), let's say with username `my_ftp_bot` +- Get bot token from BotFather's response, use it as `Token` in config +- Get bot id by run `curl https://api.telegram.org/bot/getMe` +- Find `@my_ftp_bot` in telegram and start chat with it +- Send `/start` to bot +- Run `curl https://api.telegram.org/bot/getUpdates` and find your chat id in response, use it as `ChatID` in config + + +## Config example + +Please note about `shared` flag. If it's `true` then bot instance will be shared between all connections. +If it's `false` then each user will have own bot instance and it can lead to telegram bot flood protection. + +```json +{ + "version": 1, + "accesses": [ + { + "fs": "telegram", + "shared": true, + "user": "my_ftp_bot", + "pass": "my_secure_password", + "params": { + "Token": "", + "ChatID": "" + } + + } + ], + "passive_transfer_port_range": { + "start": 2122, + "end": 2130 + } +} +``` \ No newline at end of file diff --git a/fs/telegram/example.conf b/fs/telegram/example.conf index 99524f57..3cb27ac0 100644 --- a/fs/telegram/example.conf +++ b/fs/telegram/example.conf @@ -2,9 +2,10 @@ "version": 1, "accesses": [ { + "fs": "telegram", + "shared": true, "user": "test", "pass": "test", - "fs": "telegram", "params": { "Token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789", "ChatID": "123456789" diff --git a/fs/telegram/fake_fs.go b/fs/telegram/fake_fs.go new file mode 100644 index 00000000..3d07e3dc --- /dev/null +++ b/fs/telegram/fake_fs.go @@ -0,0 +1,79 @@ +package telegram + +import ( + "os" + "sync" +) + +// fakeFilesystem is a really simple and limited fake filesystem intended for store temporary info about files +// since some ftp clients expect to perform mkdir() + stat() on files and directories before upload +type fakeFilesystem struct { + sync.Mutex + dict map[string]*FileInfo + // dir fakeDir +} + +type fakeDir struct { + name string + content []os.FileInfo +} + +// newFakeFilesystem creates a new fake filesystem +func newFakeFilesystem() *fakeFilesystem { + return &fakeFilesystem{ + dict: map[string]*FileInfo{}, + // dir: fakeDir{content: []os.FileInfo{}}, + } +} + +// mkdir creates a directory +func (f *fakeFilesystem) mkdir(name string, mode os.FileMode) { + f.Lock() + defer f.Unlock() + f.dict[name] = &FileInfo{&FileData{ + name: name, + dir: true, + mode: mode, + }} +} + +// create creates a file +func (f *fakeFilesystem) create(name string) { + f.Lock() + defer f.Unlock() + f.dict[name] = &FileInfo{&FileData{ + name: name, + dir: false, + }} +} + +// setSize sets the size of a file +func (f *fakeFilesystem) setSize(name string, size int64) { + f.Lock() + defer f.Unlock() + if fileInfo, found := f.dict[name]; found { + fileInfo.size = size + } +} + +// stat returns a file info +func (f *fakeFilesystem) stat(name string) *FileInfo { + f.Lock() + defer f.Unlock() + return f.dict[name] +} + +// remove removes a file +func (f *fakeFilesystem) remove(name string) { + f.Lock() + defer f.Unlock() + delete(f.dict, name) +} + +// exists checks if a file exists +func (f *fakeFilesystem) exists(name string) bool { + f.Lock() + defer f.Unlock() + _, ok := f.dict[name] + return ok +} diff --git a/fs/telegram/file_info.go b/fs/telegram/file_info.go new file mode 100644 index 00000000..b6d30fdc --- /dev/null +++ b/fs/telegram/file_info.go @@ -0,0 +1,37 @@ +package telegram + +import ( + "os" + "path/filepath" + "time" +) + +// FileData is a simple structure to store file information and implement os.FileInfo interface +type FileData struct { + name string + dir bool + mode os.FileMode + modtime time.Time + size int64 +} + +type FileInfo struct { + *FileData +} + +// Implements os.FileInfo +func (s *FileInfo) Name() string { + _, name := filepath.Split(s.name) + return name +} + +func (s *FileInfo) Mode() os.FileMode { return s.mode } +func (s *FileInfo) ModTime() time.Time { return s.modtime } +func (s *FileInfo) IsDir() bool { return s.dir } +func (s *FileInfo) Sys() interface{} { return nil } +func (s *FileInfo) Size() int64 { + if s.IsDir() { + return int64(42) + } + return s.size +} diff --git a/fs/telegram/telegram.go b/fs/telegram/telegram.go index 24629759..d3cfa833 100644 --- a/fs/telegram/telegram.go +++ b/fs/telegram/telegram.go @@ -33,22 +33,40 @@ var ErrInvalidParameter = errors.New("invalid parameter") // Fs is a write-only afero.Fs implementation using telegram as backend type Fs struct { - Bot *tele.Bot + // Bot is the telegram bot instance + Bot *tele.Bot + // ChatID is the telegram chat ID to send files to ChatID int64 + // Logger is the logger, obviously Logger log.Logger + + // fakeFs is a lightweight fake filesystem intended for store temporary info about files + // since some ftp clients expect to perform mkdir() + stat() on files and directories before upload + fakeFs *fakeFilesystem } // File is the afero.File implementation type File struct { - Path string + // Path is the file path + Path string + // Content is the file content Content []byte - Fs *Fs - At int64 + // Fs is the parent Fs + Fs *Fs + // At is the current position in the file + At int64 } +// imageExtensions is the list of supported image extensions var imageExtensions = []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".tif"} + +// videoExtensions is the list of supported video extensions var videoExtensions = []string{".mp4", ".avi", ".mkv", ".mov", ".wmv", ".flv", ".webm", ".mpeg", ".mpg", ".m4v", ".3gp", ".3g2"} + +// textExtensions is the list of supported text extensions var textExtensions = []string{".txt", ".md"} + +// audioExtensions is the list of supported audio extensions var audioExtensions = []string{".mp3", ".ogg", ".flac", ".wav", ".m4a", ".opus"} // LoadFs loads a file system from an access description @@ -70,6 +88,7 @@ func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) { } bot, err := tele.NewBot(pref) + logger.Info("telegram bot initialization", "token", token[:10]+"...", "chatID", chatID) if err != nil { logger.Error("telegram bot initialization", "err", err) return nil, err @@ -77,10 +96,8 @@ func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) { bot.Use(middleware.Logger()) bot.Use(middleware.AutoRespond()) - // Just to check that the bot is working - bot.Handle("/hello", func(c tele.Context) error { - return c.Send("Hello!") - }) + bot.Handle("/start", startHandler) + bot.Handle("/help", helpHandler) go func() { // Run bot in the background @@ -91,6 +108,7 @@ func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) { Bot: bot, Logger: logger, ChatID: chatID, + fakeFs: newFakeFilesystem(), } return fs, nil @@ -136,6 +154,9 @@ func (f *File) Close() error { return err } + f.Fs.fakeFs.create(f.Path) + f.Fs.fakeFs.setSize(f.Path, int64(len(f.Content))) + f.Content = []byte{} f.At = 0 @@ -187,10 +208,14 @@ func (f *File) Seek(_ int64, _ int) (int64, error) { return 0, nil } -// Stat is not implemented +// Stat for the file relies on the fake filesystem func (f *File) Stat() (os.FileInfo, error) { - f.Fs.Logger.Error("telegram File.Stat() not implemented") - return nil, ErrNotImplemented + fileInfo := f.Fs.fakeFs.stat(f.Path) + + if fileInfo == nil { + return nil, &os.PathError{Op: "stat", Path: f.Path, Err: nil} + } + return fileInfo, nil } // Sync is not implemented @@ -249,13 +274,23 @@ func (m *Fs) Remove(name string) error { return nil } -// Mkdir is not implemented +// Mkdir func (m *Fs) Mkdir(name string, mode os.FileMode) error { + m.Logger.Info("telegram Mkdir()", "name", name, "mode", mode) + m.fakeFs.mkdir(name, mode) return nil } -// MkdirAll is not implemented +// MkdirAll creates full path of directories +// like mkdir -p func (m *Fs) MkdirAll(name string, mode os.FileMode) error { + m.Logger.Info("telegram MkdirAll()", "name", name, "mode", mode) + path := strings.Split(name, "/") + for i := 0; i < len(path); i++ { + dir := strings.Join(path[:i+1], "/") + m.Logger.Info("telegram MkdirAll()", "dir", dir) + m.fakeFs.mkdir(dir, mode) + } return nil } @@ -266,6 +301,7 @@ func (m *Fs) Open(name string) (afero.File, error) { // Create creates a file buffer func (m *Fs) Create(name string) (afero.File, error) { + m.fakeFs.create(name) return &File{Path: name, Fs: m}, nil } @@ -274,9 +310,15 @@ func (m *Fs) OpenFile(name string, flag int, mode os.FileMode) (afero.File, erro return &File{Path: name, Fs: m}, nil } -// Stat is not implemented +// Stat() fake implementation func (m *Fs) Stat(name string) (os.FileInfo, error) { - return nil, &os.PathError{Op: "stat", Path: name, Err: nil} + fileInfo := m.fakeFs.stat(name) + m.Logger.Info("telegram Stat()", "name", name, "fileInfo", fmt.Sprintf("%#v", fileInfo)) + + if fileInfo == nil { + return nil, &os.PathError{Op: "stat", Path: name, Err: nil} + } + return fileInfo, nil } // LstatIfPossible is not implemented @@ -293,3 +335,30 @@ func isExtension(filename string, extensions []string) bool { } return false } + +const readMeURL = "https://github.com/slayer/ftpserver" + +// /start command handler +func startHandler(c tele.Context) error { + err := helpHandler(c) + if err != nil { + return err + } + var chatID int64 + chat := c.Chat() + if chat != nil { + chatID = chat.ID + } + + err = c.Send(fmt.Sprintf("Current `ChatID` is `%d`", chatID), tele.ModeMarkdown) + return err +} + +func helpHandler(c tele.Context) error { + firstName := "" + if c.Sender() != nil { + firstName = c.Sender().FirstName + } + message := fmt.Sprintf("Hello %s!, you can read more about me at %s", firstName, readMeURL) + return c.Send(message) +} From 5e6bf7d58c30650c5fe80cc5e083948635e2bc6e Mon Sep 17 00:00:00 2001 From: Vlad Date: Fri, 24 Nov 2023 12:31:44 +0200 Subject: [PATCH 4/7] update doc; remove debug logging --- fs/telegram/README.md | 6 ++++-- fs/telegram/telegram.go | 6 +----- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fs/telegram/README.md b/fs/telegram/README.md index 8750d89b..07bde2a7 100644 --- a/fs/telegram/README.md +++ b/fs/telegram/README.md @@ -2,7 +2,9 @@ ## Register bot -Read about telegram bots at https://core.telegram.org/bots/tutorial +Read about telegram bots at https://core.telegram.org/bots/tutorial. + +Bots are not allowed to contact users. You need to make the first contact from the user for which you want to set up the bot. ### Quick start @@ -17,7 +19,7 @@ Read about telegram bots at https://core.telegram.org/bots/tutorial ## Config example Please note about `shared` flag. If it's `true` then bot instance will be shared between all connections. -If it's `false` then each user will have own bot instance and it can lead to telegram bot flood protection. +If it's `false` then each user (or even each ftp connection) will have own bot instance and it can lead to telegram bot flood protection. ```json { diff --git a/fs/telegram/telegram.go b/fs/telegram/telegram.go index d3cfa833..1a55f974 100644 --- a/fs/telegram/telegram.go +++ b/fs/telegram/telegram.go @@ -88,7 +88,6 @@ func LoadFs(access *confpar.Access, logger log.Logger) (afero.Fs, error) { } bot, err := tele.NewBot(pref) - logger.Info("telegram bot initialization", "token", token[:10]+"...", "chatID", chatID) if err != nil { logger.Error("telegram bot initialization", "err", err) return nil, err @@ -148,6 +147,7 @@ func (f *File) Close() error { document.FileLocal = basePath _, err = f.Fs.Bot.Send(&chat, &document) } + f.Fs.Logger.Info("telegram Bot.Send()", "path", f.Path) if err != nil { f.Fs.Logger.Error("telegram Bot.Send()", "err", err) @@ -276,7 +276,6 @@ func (m *Fs) Remove(name string) error { // Mkdir func (m *Fs) Mkdir(name string, mode os.FileMode) error { - m.Logger.Info("telegram Mkdir()", "name", name, "mode", mode) m.fakeFs.mkdir(name, mode) return nil } @@ -284,11 +283,9 @@ func (m *Fs) Mkdir(name string, mode os.FileMode) error { // MkdirAll creates full path of directories // like mkdir -p func (m *Fs) MkdirAll(name string, mode os.FileMode) error { - m.Logger.Info("telegram MkdirAll()", "name", name, "mode", mode) path := strings.Split(name, "/") for i := 0; i < len(path); i++ { dir := strings.Join(path[:i+1], "/") - m.Logger.Info("telegram MkdirAll()", "dir", dir) m.fakeFs.mkdir(dir, mode) } return nil @@ -313,7 +310,6 @@ func (m *Fs) OpenFile(name string, flag int, mode os.FileMode) (afero.File, erro // Stat() fake implementation func (m *Fs) Stat(name string) (os.FileInfo, error) { fileInfo := m.fakeFs.stat(name) - m.Logger.Info("telegram Stat()", "name", name, "fileInfo", fmt.Sprintf("%#v", fileInfo)) if fileInfo == nil { return nil, &os.PathError{Op: "stat", Path: name, Err: nil} From 1b8f024fa6c7e997709b272b2f361c3dabd5eb81 Mon Sep 17 00:00:00 2001 From: Vlad Date: Fri, 24 Nov 2023 12:37:14 +0200 Subject: [PATCH 5/7] actions/checkout@v4.1.1 --- .github/workflows/goreleaser.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index 9ae3bea3..2a4f9e7a 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.1.1.1 + uses: actions/checkout@v4.1.1 with: fetch-depth: 0 - From 8b151429dd8e22f71f0ae9d1a3ff8a365713e6e3 Mon Sep 17 00:00:00 2001 From: Vlad Date: Fri, 24 Nov 2023 12:52:11 +0200 Subject: [PATCH 6/7] goreleaser: add goarch, goarm params --- .goreleaser.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 8a2cf155..437ab5e8 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,3 +1,10 @@ builds: - ldflags: - -s -w -X main.BuildVersion={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.Date}} + - goarch: + - amd64 + - arm + - arm64 + - goarm: + - 6 + - 7 From 9b19235f58cd4e649f757140a7ace8febf9c6e62 Mon Sep 17 00:00:00 2001 From: Vlad Date: Fri, 24 Nov 2023 12:56:57 +0200 Subject: [PATCH 7/7] goreleaser: add goarch, goarm params --- .goreleaser.yaml | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 437ab5e8..09a116cf 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,10 +1,17 @@ builds: - - ldflags: + - env: + - CGO_ENABLED=0 + - GO111MODULE=on + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm + - arm64 + goarm: + - "6" + - "7" + ldflags: - -s -w -X main.BuildVersion={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.Date}} - - goarch: - - amd64 - - arm - - arm64 - - goarm: - - 6 - - 7