Skip to content

Commit

Permalink
Adding telegram connector (#1190)
Browse files Browse the repository at this point in the history
* Telegram connector (#1)

* Initial telegram interface implementation

* bump go version to 1.21.3

* update doc; remove debug logging

* actions/[email protected]

* goreleaser: add goarch, goarm params

* sync fork (#2)

* sync fork

* SSL certs (#3)

* SSL certs required to connect telegram API

---------

Co-authored-by: Vlad <[email protected]>
Co-authored-by: Vlad <[email protected]>
  • Loading branch information
3 people authored May 28, 2024
1 parent 831c76c commit e58ef15
Show file tree
Hide file tree
Showing 14 changed files with 665 additions and 135 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -29,7 +29,7 @@ jobs:
- name: Setup go
uses: actions/[email protected]
with:
go-version: 1.19
go-version: 1.21.3

- name: Build
run: go build -v ./...
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ jobs:
steps:
-
name: Checkout
uses: actions/[email protected].1.1
uses: actions/[email protected]
with:
fetch-depth: 0
-
Expand Down
16 changes: 15 additions & 1 deletion .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +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}}
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Preparing the build environment
FROM golang:1.22-alpine AS builder
ENV GOFLAGS="-mod=readonly"
RUN apk add --update --no-cache bash ca-certificates curl git
RUN apk add --update --no-cache bash ca-certificates curl git && update-ca-certificates
RUN mkdir -p /workspace
WORKDIR /workspace

Expand All @@ -15,6 +15,7 @@ RUN CGO_ENABLED=0 go build -mod=readonly -ldflags='-w -s' -v -o ftpserver
# Preparing the final image
FROM scratch
WORKDIR /app
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
EXPOSE 2121-2130
COPY --from=builder /workspace/ftpserver /bin/ftpserver
ENTRYPOINT [ "/bin/ftpserver" ]
2 changes: 1 addition & 1 deletion Dockerfile.alpine
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# Preparing the build environment
FROM golang:1.22-alpine AS builder
ENV GOFLAGS="-mod=readonly"
RUN apk add --update --no-cache bash ca-certificates curl git
RUN apk add --update --no-cache bash ca-certificates curl git && update-ca-certificates
RUN mkdir -p /workspace
WORKDIR /workspace

Expand Down
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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), 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)).

Expand Down Expand Up @@ -191,6 +192,16 @@ Here is a sample config file:
"password": "password",
"hostname": "192.168.168.11:22"
}
},
{
"user": "telegram",
"pass": "telegram",
"fs": "telegram",
"shared": true,
"params": {
"Token": "<OBTAIN_TOKEN_FROM_BOTFATHER>",
"ChatID": "<INSERT_CHAT_ID_HERE>"
}
}
]
}
Expand Down
3 changes: 3 additions & 0 deletions fs/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}
}
Expand Down
47 changes: 47 additions & 0 deletions fs/telegram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# FTPServer Telegram connector

[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)

## Register bot

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

- 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<YOUR_BOT_TOKEN>/getMe`
- Find `@my_ftp_bot` in telegram and start chat with it
- Send `/start` to bot
- Run `curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/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 (or even each ftp connection) 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": "<YOUR_BOT_TOKEN>",
"ChatID": "<YOUR_CHAT_ID>"
}

}
],
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
}
}
```
20 changes: 20 additions & 0 deletions fs/telegram/example.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"version": 1,
"accesses": [
{
"fs": "telegram",
"shared": true,
"user": "test",
"pass": "test",
"params": {
"Token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789",
"ChatID": "123456789"
}

}
],
"passive_transfer_port_range": {
"start": 2122,
"end": 2130
}
}
79 changes: 79 additions & 0 deletions fs/telegram/fake_fs.go
Original file line number Diff line number Diff line change
@@ -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
}
37 changes: 37 additions & 0 deletions fs/telegram/file_info.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit e58ef15

Please sign in to comment.