Skip to content

Commit e58ef15

Browse files
fclairambslayer
andauthored
Adding telegram connector (#1190)
* Telegram connector (#1) * Initial telegram interface implementation * bump go version to 1.21.3 * update doc; remove debug logging * actions/checkout@v4.1.1 * goreleaser: add goarch, goarm params * sync fork (#2) * sync fork * SSL certs (#3) * SSL certs required to connect telegram API --------- Co-authored-by: Vlad <57521+slayer@users.noreply.github.com> Co-authored-by: Vlad <devvlad@gmail.com>
1 parent 831c76c commit e58ef15

14 files changed

Lines changed: 665 additions & 135 deletions

File tree

.github/workflows/build.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ jobs:
1212
build:
1313
# The type of runner that the job will run on
1414
runs-on: ubuntu-22.04
15-
15+
1616
# Steps represent a sequence of tasks that will be executed as part of the job
1717
steps:
1818
# Checks-out your repository under $GITHUB_WORKSPACE
@@ -29,7 +29,7 @@ jobs:
2929
- name: Setup go
3030
uses: actions/setup-go@v5.0.1
3131
with:
32-
go-version: 1.19
32+
go-version: 1.21.3
3333

3434
- name: Build
3535
run: go build -v ./...

.github/workflows/goreleaser.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
steps:
1212
-
1313
name: Checkout
14-
uses: actions/checkout@v4.1.1.1.1
14+
uses: actions/checkout@v4.1.1
1515
with:
1616
fetch-depth: 0
1717
-

.goreleaser.yaml

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
11
builds:
2-
- ldflags:
2+
- env:
3+
- CGO_ENABLED=0
4+
- GO111MODULE=on
5+
goos:
6+
- linux
7+
- darwin
8+
- windows
9+
goarch:
10+
- amd64
11+
- arm
12+
- arm64
13+
goarm:
14+
- "6"
15+
- "7"
16+
ldflags:
317
- -s -w -X main.BuildVersion={{.Version}} -X main.Commit={{.Commit}} -X main.BuildDate={{.Date}}

Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Preparing the build environment
55
FROM golang:1.22-alpine AS builder
66
ENV GOFLAGS="-mod=readonly"
7-
RUN apk add --update --no-cache bash ca-certificates curl git
7+
RUN apk add --update --no-cache bash ca-certificates curl git && update-ca-certificates
88
RUN mkdir -p /workspace
99
WORKDIR /workspace
1010

@@ -15,6 +15,7 @@ RUN CGO_ENABLED=0 go build -mod=readonly -ldflags='-w -s' -v -o ftpserver
1515
# Preparing the final image
1616
FROM scratch
1717
WORKDIR /app
18+
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
1819
EXPOSE 2121-2130
1920
COPY --from=builder /workspace/ftpserver /bin/ftpserver
2021
ENTRYPOINT [ "/bin/ftpserver" ]

Dockerfile.alpine

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Preparing the build environment
55
FROM golang:1.22-alpine AS builder
66
ENV GOFLAGS="-mod=readonly"
7-
RUN apk add --update --no-cache bash ca-certificates curl git
7+
RUN apk add --update --no-cache bash ca-certificates curl git && update-ca-certificates
88
RUN mkdir -p /workspace
99
WORKDIR /workspace
1010

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ At the current stage, supported backend are:
1818
- [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)
1919
- [SFTP](https://en.wikipedia.org/wiki/SSH_File_Transfer_Protocol) through [afero's sftpfs](https://github.com/spf13/afero/)
2020
- Email through [go-mail](https://github.com/go-mail/mail) thanks to [@x-way](https://github.com/x-way)
21+
- Telegram through [telebot](https://github.com/tucnak/telebot) by [@slayer](https://github.com/slayer), see [doc](fs/telegram/README.md)
2122

2223
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)).
2324

@@ -191,6 +192,16 @@ Here is a sample config file:
191192
"password": "password",
192193
"hostname": "192.168.168.11:22"
193194
}
195+
},
196+
{
197+
"user": "telegram",
198+
"pass": "telegram",
199+
"fs": "telegram",
200+
"shared": true,
201+
"params": {
202+
"Token": "<OBTAIN_TOKEN_FROM_BOTFATHER>",
203+
"ChatID": "<INSERT_CHAT_ID_HERE>"
204+
}
194205
}
195206
]
196207
}

fs/fs.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"github.com/fclairamb/ftpserver/fs/mail"
1616
"github.com/fclairamb/ftpserver/fs/s3"
1717
"github.com/fclairamb/ftpserver/fs/sftp"
18+
"github.com/fclairamb/ftpserver/fs/telegram"
1819
)
1920

2021
// 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) {
4546
fs, err = gdrive.LoadFs(access, logger.With("component", "gdrive"))
4647
case "dropbox":
4748
fs, err = dropbox.LoadFs(access)
49+
case "telegram":
50+
fs, err = telegram.LoadFs(access, logger.With("component", "telegram"))
4851
default:
4952
fs, err = nil, &UnsupportedFsError{Type: access.Fs}
5053
}

fs/telegram/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# FTPServer Telegram connector
2+
3+
[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua)
4+
5+
## Register bot
6+
7+
Read about telegram bots at https://core.telegram.org/bots/tutorial.
8+
9+
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.
10+
11+
### Quick start
12+
13+
- Create a bot with [@BotFather](https://t.me/BotFather), let's say with username `my_ftp_bot`
14+
- Get bot token from BotFather's response, use it as `Token` in config
15+
- Get bot id by run `curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getMe`
16+
- Find `@my_ftp_bot` in telegram and start chat with it
17+
- Send `/start` to bot
18+
- Run `curl https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getUpdates` and find your chat id in response, use it as `ChatID` in config
19+
20+
21+
## Config example
22+
23+
Please note about `shared` flag. If it's `true` then bot instance will be shared between all connections.
24+
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.
25+
26+
```json
27+
{
28+
"version": 1,
29+
"accesses": [
30+
{
31+
"fs": "telegram",
32+
"shared": true,
33+
"user": "my_ftp_bot",
34+
"pass": "my_secure_password",
35+
"params": {
36+
"Token": "<YOUR_BOT_TOKEN>",
37+
"ChatID": "<YOUR_CHAT_ID>"
38+
}
39+
40+
}
41+
],
42+
"passive_transfer_port_range": {
43+
"start": 2122,
44+
"end": 2130
45+
}
46+
}
47+
```

fs/telegram/example.conf

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
{
2+
"version": 1,
3+
"accesses": [
4+
{
5+
"fs": "telegram",
6+
"shared": true,
7+
"user": "test",
8+
"pass": "test",
9+
"params": {
10+
"Token": "123456789:ABCDEFGHIJKLMNOPQRSTUVWXYZ123456789",
11+
"ChatID": "123456789"
12+
}
13+
14+
}
15+
],
16+
"passive_transfer_port_range": {
17+
"start": 2122,
18+
"end": 2130
19+
}
20+
}

fs/telegram/fake_fs.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package telegram
2+
3+
import (
4+
"os"
5+
"sync"
6+
)
7+
8+
// fakeFilesystem is a really simple and limited fake filesystem intended for store temporary info about files
9+
// since some ftp clients expect to perform mkdir() + stat() on files and directories before upload
10+
type fakeFilesystem struct {
11+
sync.Mutex
12+
dict map[string]*FileInfo
13+
// dir fakeDir
14+
}
15+
16+
type fakeDir struct {
17+
name string
18+
content []os.FileInfo
19+
}
20+
21+
// newFakeFilesystem creates a new fake filesystem
22+
func newFakeFilesystem() *fakeFilesystem {
23+
return &fakeFilesystem{
24+
dict: map[string]*FileInfo{},
25+
// dir: fakeDir{content: []os.FileInfo{}},
26+
}
27+
}
28+
29+
// mkdir creates a directory
30+
func (f *fakeFilesystem) mkdir(name string, mode os.FileMode) {
31+
f.Lock()
32+
defer f.Unlock()
33+
f.dict[name] = &FileInfo{&FileData{
34+
name: name,
35+
dir: true,
36+
mode: mode,
37+
}}
38+
}
39+
40+
// create creates a file
41+
func (f *fakeFilesystem) create(name string) {
42+
f.Lock()
43+
defer f.Unlock()
44+
f.dict[name] = &FileInfo{&FileData{
45+
name: name,
46+
dir: false,
47+
}}
48+
}
49+
50+
// setSize sets the size of a file
51+
func (f *fakeFilesystem) setSize(name string, size int64) {
52+
f.Lock()
53+
defer f.Unlock()
54+
if fileInfo, found := f.dict[name]; found {
55+
fileInfo.size = size
56+
}
57+
}
58+
59+
// stat returns a file info
60+
func (f *fakeFilesystem) stat(name string) *FileInfo {
61+
f.Lock()
62+
defer f.Unlock()
63+
return f.dict[name]
64+
}
65+
66+
// remove removes a file
67+
func (f *fakeFilesystem) remove(name string) {
68+
f.Lock()
69+
defer f.Unlock()
70+
delete(f.dict, name)
71+
}
72+
73+
// exists checks if a file exists
74+
func (f *fakeFilesystem) exists(name string) bool {
75+
f.Lock()
76+
defer f.Unlock()
77+
_, ok := f.dict[name]
78+
return ok
79+
}

0 commit comments

Comments
 (0)