├── .github └── workflows │ └── go.yml ├── LICENSE ├── README.md ├── api └── types.go ├── go.mod ├── rclone.conf └── telegram.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.19 23 | 24 | - name: Tidy 25 | run: go mod tidy -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Bullseyecowbelly 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Telegram Backend for RClone (Still Under Development) *Broken at the moment* 2 | 3 | # Project is Dead 4 | 5 | This is a backend for [RClone](https://rclone.org/) that allows you to use Telegram as a remote storage service. 6 | 7 | ## Installation 8 | 9 | To use this backend, you need to have RClone installed. You can download the latest version of RClone from their [official website](https://rclone.org/downloads/). 10 | 11 | To use this backend, you also need to have a Telegram bot and chat ID. You can create a bot and obtain the chat ID by following the instructions in [Telegram's Bot API documentation](https://core.telegram.org/bots#creating-a-new-bot). 12 | 13 | Once you have RClone and a Telegram bot set up, you can install this backend by running the following command: 14 | 15 | ``` 16 | go install github.com/greengeckowizard/RcloneTelegram@latest 17 | ``` 18 | 19 | ## Configuration 20 | 21 | To use this backend, you need to add it to your RClone configuration file. You can do this by running the following command: 22 | 23 | ``` 24 | rclone config 25 | ``` 26 | 27 | Then, follow the prompts to configure the backend. You will need to provide your Telegram bot token and chat ID. 28 | 29 | Here's an example configuration: 30 | 31 | ``` 32 | [mytelegram] 33 | type = telegram 34 | token = 35 | chat_id = 36 | ``` 37 | 38 | You can replace "mytelegram" with any name you like. 39 | 40 | ## How to Test 41 | 42 | Since this code is still in development, it is recommended that you test it thoroughly before using it for production purposes. 43 | 44 | To test the backend, you can try running the following RClone commands: 45 | 46 | ``` 47 | rclone lsf mytelegram:/ 48 | ``` 49 | 50 | This should list the contents of your Telegram chat. 51 | 52 | ``` 53 | rclone copy /path/to/local/file mytelegram:/remote/path 54 | ``` 55 | 56 | This should upload the local file to your Telegram chat. 57 | 58 | ``` 59 | rclone copy mytelegram:/remote/path /path/to/local/file 60 | ``` 61 | 62 | This should download the remote file from your Telegram chat to the local file system. 63 | 64 | Note that the Telegram backend only supports files up to 2GB in size. 65 | 66 | 67 | -------------------------------------------------------------------------------- /api/types.go: -------------------------------------------------------------------------------- 1 | export namespace Api { 2 | export type InputPeer = { 3 | _: 'inputPeer' 4 | user_id: number 5 | access_hash: string 6 | } 7 | 8 | export type InputUser = { 9 | _: 'inputUser' 10 | user_id: number 11 | access_hash: string 12 | } 13 | 14 | export type InputChannel = { 15 | _: 'inputChannel' 16 | channel_id: number 17 | access_hash: string 18 | } 19 | 20 | export type InputMessageID = { 21 | _: 'inputMessageID' 22 | id: number 23 | } 24 | 25 | export type MessagesGetMessages = { 26 | _: 'messages.getMessages' 27 | id: InputMessageID[] 28 | } 29 | 30 | export type MessagesGetMessagesResponse = { 31 | messages: Message[] 32 | } 33 | 34 | export type Message = { 35 | _: 'message' 36 | id: number 37 | media: Media 38 | } 39 | 40 | export type Media = { 41 | _: 'messageMediaDocument' 42 | document: Document 43 | } 44 | 45 | export type Document = { 46 | _: 'document' 47 | id: string 48 | access_hash: string 49 | size: number 50 | mime_type: string 51 | file_reference: Uint8Array 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module rclonetelegram 2 | 3 | require github.com/rclone/rclone v1.55.1 4 | 5 | 6 | go 1.20 7 | -------------------------------------------------------------------------------- /rclone.conf: -------------------------------------------------------------------------------- 1 | [mytelegram] 2 | type = telegram 3 | token = 4 | chat_id = 5 | -------------------------------------------------------------------------------- /telegram.go: -------------------------------------------------------------------------------- 1 | package telegram 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "strings" 8 | 9 | "github.com/rclone/rclone/backend" 10 | "github.com/rclone/rclone/fs" 11 | "github.com/rclone/rclone/fs/config/configmap" 12 | "github.com/rclone/rclone/fs/config/configstruct" 13 | "github.com/rclone/rclone/fs/fshttp" 14 | "github.com/rclone/rclone/fs/hash" 15 | "github.com/rclone/rclone/lib/readers" 16 | "github.com/rclone/rclone/lib/timedata" 17 | "github.com/rclone/rclone/vfs" 18 | "gopkg.in/telebot.v3" 19 | ) 20 | 21 | // Register with Fs 22 | func init() { 23 | fs.Register(&fs.RegInfo{ 24 | Name: "telegram", 25 | Description: "Telegram", 26 | NewFs: NewFs, 27 | Config: func(ctx context.Context, name string, m configmap.Mapper, config string) error { 28 | return configstruct.Set(m, &Options) 29 | }, 30 | Options: []fs.Option{{ 31 | Name: "token", 32 | Help: "Telegram bot token.", 33 | Required: true, 34 | }, { 35 | Name: "chat_id", 36 | Help: "Telegram chat ID.", 37 | Required: true, 38 | }}, 39 | }) 40 | } 41 | 42 | // Options defines the configuration for this backend 43 | type Options struct { 44 | Token string `config:"token"` 45 | ChatID string `config:"chat_id"` 46 | } 47 | 48 | // Fs represents a remote Telegram chat 49 | type Fs struct { 50 | name string 51 | root string 52 | features *vfs.Features 53 | bot *telebot.Bot 54 | chatID int64 55 | } 56 | 57 | // NewFs constructs a new Fs 58 | func NewFs(name string, m configmap.Mapper) (fs.Fs, error) { 59 | var options Options 60 | if err := configstruct.Decode(m, &options); err != nil { 61 | return nil, err 62 | } 63 | 64 | bot, err := telebot.NewBot(telebot.Settings{Token: options.Token}) 65 | if err != nil { 66 | return nil, err 67 | } 68 | me, err := bot.Me() 69 | if err != nil { 70 | return nil, err 71 | } 72 | chatID, err := me.ChatID() 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | f := &Fs{ 78 | name: name, 79 | root: "/", 80 | features: vfs.NewFeatures(), 81 | bot: bot, 82 | chatID: chatID, 83 | } 84 | 85 | return f, nil 86 | } 87 | 88 | // Name of the remote (as passed into NewFs) 89 | func (f *Fs) Name() string { 90 | return f.name 91 | } 92 | 93 | // Root of the remote (as passed into NewFs) 94 | func (f *Fs) Root() string { 95 | return f.root 96 | } 97 | 98 | // String returns a description of the FS 99 | func (f *Fs) String() string { 100 | return "Telegram chat " + f.name 101 | } 102 | 103 | // Features returns the optional features of this Fs 104 | func (f *Fs) Features() *vfs.Features { 105 | return f.features 106 | } 107 | 108 | // Hashes returns the supported hash sets 109 | func (f *Fs) Hashes() hash.Set { 110 | return hash.Set(hash.None) 111 | } 112 | 113 | // Put uploads contents to the remote path 114 | func (f *Fs) Put(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 115 | if src.Size() > int64(2<<30) { 116 | return nil, errors.New("telegram backend only supports files up to 2GB in size") 117 | } 118 | 119 | fileName := src.Remote() 120 | if strings.HasPrefix(fileName, "/") { 121 | fileName = fileName[1:] 122 | } 123 | 124 | file := telebot.Document{ 125 | File: telebot.FromReader(readers.NewLimited(in, src.Size())), 126 | Caption: fileName, 127 | } 128 | 129 | message, err := f.bot.Send(f.chatID, &file) 130 | if err != nil { 131 | return nil, err 132 | } 133 | 134 | return &Object{ 135 | fs: f, 136 | path: "/" + message.Document.FileID, 137 | name: fileName, 138 | size: src.Size(), 139 | modTime: message.Date, 140 | isDir: false, 141 | }, nil 142 | } 143 | 144 | // List returns a channel to the objects and subdirectories 145 | // in dir with directory entries popped from the channel 146 | func (f *Fs) List(ctx context.Context, dir string) (fs.DirChan, fs.EntryChan, error) { 147 | return nil, nil, errors.New("telegram backend does not support directory listing") 148 | } 149 | 150 | // NewObject finds the Object at remote 151 | func (f *Fs) NewObject(ctx context.Context, remote string) (fs.Object, error) { 152 | message, err := f.bot.GetFile(remote[1:]) 153 | if err != nil { 154 | return nil, err 155 | } 156 | 157 | return &Object{ 158 | fs: f, 159 | path: remote, 160 | name: message.FilePath, 161 | size: int64(message.FileSize), 162 | modTime: timedata.FromUnix(message.Date), 163 | isDir: false, 164 | }, nil 165 | } 166 | 167 | // PutStream uploads contents to the remote path using a stream 168 | func (f *Fs) PutStream(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) (fs.Object, error) { 169 | return f.Put(ctx, in, src, options...) 170 | } 171 | 172 | // Mkdir creates the directory if it doesn't exist 173 | func (f *Fs) Mkdir(ctx context.Context, dir string) error { 174 | return errors.New("telegram backend does not support directory creation") 175 | } 176 | 177 | // Rmdir removes the directory 178 | func (f *Fs) Rmdir(ctx context.Context, dir string) error { 179 | return errors.New("telegram backend does not support directory removal") 180 | } 181 | 182 | // Precision of the ModTimes in this Fs 183 | func (f *Fs) Precision() time.Duration { 184 | return fs.ModTimeNotSupported 185 | } 186 | 187 | // Object represents a remote Telegram file 188 | type Object struct { 189 | fs *Fs 190 | path string 191 | name string 192 | size int64 193 | modTime time.Time 194 | isDir bool 195 | } 196 | 197 | // Fs returns the parent Fs 198 | func (o *Object) Fs() fs.Info { 199 | return o.fs 200 | } 201 | 202 | // String returns a description of the Object 203 | func (o *Object) String() string { 204 | return o.path 205 | } 206 | 207 | // Remote returns the remote path 208 | func (o *Object) Remote() string { 209 | return o.path 210 | } 211 | 212 | // Hash returns the selected checksum of the file 213 | // If no checksum is available it returns "" 214 | func (o *Object) Hash(r hash.Type) (string, error) { 215 | return "", hash.ErrUnsupported 216 | } 217 | 218 | // Size returns the size of the file 219 | func (o *Object) Size() int64 { 220 | return o.size 221 | } 222 | 223 | // ModTime returns the modification time of the file 224 | func (o *Object) ModTime(ctx context.Context) time.Time { 225 | return o.modTime 226 | } 227 | 228 | // SetModTime sets the modification time of the file 229 | func (o *Object) SetModTime(ctx context.Context, modTime time.Time) error { 230 | return errors.New("telegram backend does not support modification times") 231 | } 232 | 233 | // Storable returns whether this object can be stored 234 | func (o *Object) Storable() bool { 235 | return true 236 | } 237 | 238 | // Open opens the file for read 239 | func (o *Object) Open(ctx context.Context, options ...fs.OpenOption) (io.ReadCloser, error) { 240 | fileBytes, err := o.fs.bot.Download(&telebot.File{FileID: o.path[1:]}) 241 | if err != nil { 242 | return nil, err 243 | } 244 | 245 | return fileBytes, nil 246 | } 247 | 248 | // Update updates the object from in with modTime 249 | func (o *Object) Update(ctx context.Context, in io.Reader, src fs.ObjectInfo, options ...fs.OpenOption) error { 250 | return errors.New("telegram backend does not support object updates") 251 | } 252 | 253 | // Remove deletes the remote object 254 | func (o *Object) Remove(ctx context.Context) error { 255 | _, err := o.fs.bot.Delete(&telebot.Message{Document: &telebot.Document{FileID: o.path[1:]}}) 256 | return err 257 | } 258 | --------------------------------------------------------------------------------