├── .gitignore ├── LICENSE ├── README.md ├── agent.go ├── dir.go ├── doc.go ├── download.go ├── example_test.go ├── file.go ├── go.mod ├── go.sum ├── import.go ├── internal ├── crypto │ ├── ec115 │ │ ├── api.go │ │ └── cipher.go │ ├── hash │ │ ├── digest.go │ │ ├── encode.go │ │ └── short.go │ ├── lz4 │ │ ├── block.go │ │ └── buffer.go │ └── m115 │ │ ├── public.go │ │ ├── rsa.go │ │ ├── util.go │ │ └── xor.go ├── impl │ ├── api.go │ ├── body.go │ ├── client.go │ ├── common.go │ ├── cookie.go │ ├── default.go │ ├── header.go │ ├── payload.go │ └── valve.go ├── multipart │ ├── boundary.go │ ├── builder.go │ └── form.go ├── oss │ ├── callback.go │ ├── date.go │ ├── header.go │ ├── sign.go │ ├── upload.go │ └── util.go ├── protocol │ ├── basic.go │ ├── cookie.go │ ├── dir.go │ ├── file.go │ ├── label.go │ ├── name.go │ ├── offline.go │ ├── qrcode.go │ ├── recycle.go │ ├── share.go │ ├── upload.go │ └── video.go ├── upload │ ├── signature.go │ └── token.go └── util │ ├── base64.go │ ├── cookie.go │ ├── io.go │ ├── json.go │ ├── mime.go │ ├── params.go │ ├── time.go │ ├── url.go │ └── value.go ├── iterator.go ├── label.go ├── login.go ├── lowlevel.go ├── lowlevel ├── api │ ├── app.go │ ├── basic.go │ ├── dir.go │ ├── download.go │ ├── file.go │ ├── hidden.go │ ├── image.go │ ├── index.go │ ├── json.go │ ├── label.go │ ├── m115.go │ ├── offline.go │ ├── qrcode.go │ ├── recycle.go │ ├── share.go │ ├── shortcut.go │ ├── upload.go │ ├── user.go │ └── video.go ├── client │ ├── client.go │ └── types.go ├── doc.go ├── errors │ ├── errors.go │ ├── fault.go │ ├── file.go │ ├── get.go │ ├── media.go │ ├── offline.go │ └── share.go └── types │ ├── agent.go │ ├── app.go │ ├── download.go │ ├── file.go │ ├── image.go │ ├── index.go │ ├── label.go │ ├── offline.go │ ├── qrcode.go │ ├── recycle.go │ ├── share.go │ ├── shortcut.go │ ├── upload.go │ ├── user.go │ ├── video.go │ └── void.go ├── media.go ├── offline.go ├── option ├── agent.go ├── file.go ├── offline.go └── qrcode.go ├── order.go ├── plugin ├── doc.go ├── http.go └── logger.go ├── qrcode.go ├── star.go ├── storage.go ├── upload.go └── version.go /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | 4 | self_test.go 5 | *_self_test.go 6 | 7 | test/* -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Tomo Kunagisa 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElevenGo 2 | 3 | ![Version](https://img.shields.io/badge/release-v0.7.2-brightgreen?style=flat-square) 4 | [![Reference](https://img.shields.io/badge/Go-Reference-blue.svg?style=flat-square)](https://pkg.go.dev/github.com/deadblue/elevengo) 5 | ![License](https://img.shields.io/:License-MIT-green.svg?style=flat-square) 6 | 7 | An API client of 115 Cloud Storage Service. 8 | 9 | ## Example 10 | 11 |
12 | 13 | High-level API 14 | 15 | ```go 16 | package main 17 | 18 | import ( 19 | "github.com/deadblue/elevengo" 20 | "log" 21 | ) 22 | 23 | func main() { 24 | // Initialize agent 25 | agent := elevengo.Default() 26 | // Import credential 27 | credential := &elevengo.Credential{ 28 | UID: "", CID: "", KID: "", SEID: "", 29 | } 30 | if err := agent.CredentialImport(credential); err != nil { 31 | log.Fatalf("Import credentail error: %s", err) 32 | } 33 | 34 | // Iterate files under specific directory 35 | if it, err := agent.FileIterate("dirId"); err != nil { 36 | log.Fatalf("Iterate files error: %s", err) 37 | } else { 38 | log.Printf("Total files: %d", it.Count()) 39 | for index, file := range it.Items() { 40 | log.Printf("%d => %#v", index, file) 41 | } 42 | } 43 | 44 | } 45 | ``` 46 | 47 |
48 | 49 |
50 | 51 | Low-level API 52 | 53 | ```go 54 | package main 55 | 56 | import ( 57 | "context" 58 | "log" 59 | 60 | "github.com/deadblue/elevengo" 61 | "github.com/deadblue/elevengo/lowlevel/api" 62 | ) 63 | 64 | func main() { 65 | // Initialize agent 66 | agent := elevengo.Default() 67 | // Import credential 68 | credential := &elevengo.Credential{ 69 | UID: "", CID: "", KID: "", SEID: "", 70 | } 71 | if err := agent.CredentialImport(credential); err != nil { 72 | log.Fatalf("Import credentail error: %s", err) 73 | } 74 | 75 | // Get low-level API client 76 | llc := agent.LowlevelClient() 77 | // Init FileList API spec 78 | spec := (&api.FiieListSpec{}).Init("dirId", 0, 32) 79 | // Call API 80 | if err = llc.CallApi(spec, context.Background()); err != nil { 81 | log.Fatalf("Call API error: %s", err) 82 | } 83 | // Parse API result 84 | for index, file := range spec.Result.Files { 85 | log.Printf("File: %d => %v", index, file) 86 | } 87 | 88 | } 89 | ``` 90 |
91 | 92 | ## License 93 | 94 | MIT 95 | -------------------------------------------------------------------------------- /agent.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deadblue/elevengo/internal/impl" 7 | "github.com/deadblue/elevengo/internal/protocol" 8 | "github.com/deadblue/elevengo/internal/util" 9 | "github.com/deadblue/elevengo/lowlevel/api" 10 | "github.com/deadblue/elevengo/lowlevel/client" 11 | "github.com/deadblue/elevengo/lowlevel/types" 12 | "github.com/deadblue/elevengo/option" 13 | ) 14 | 15 | // Agent holds signed-in user's credentials, and provides methods to access upstream 16 | // server's features, such as file management, offline download, etc. 17 | type Agent struct { 18 | // Low-level API client 19 | llc *impl.ClientImpl 20 | 21 | // Common parameters 22 | common types.CommonParams 23 | 24 | // Is agent use web credential? 25 | isWeb bool 26 | } 27 | 28 | // Default creates an Agent with default settings. 29 | func Default() *Agent { 30 | return New() 31 | } 32 | 33 | // New creates Agent with customized options. 34 | func New(options ...*option.AgentOptions) *Agent { 35 | opts := util.NotNull(options...) 36 | if opts == nil { 37 | opts = option.Agent() 38 | } 39 | llc := impl.NewClient(opts.HttpClient, opts.CooldownMinMs, opts.CooldownMaxMs) 40 | appVer := opts.Version 41 | if appVer == "" { 42 | appVer, _ = getLatestAppVersion(llc, api.AppBrowserWindows) 43 | } 44 | llc.SetUserAgent(protocol.MakeUserAgent(opts.Name, api.AppNameBrowser, appVer)) 45 | return &Agent{ 46 | llc: llc, 47 | common: types.CommonParams{ 48 | AppVer: appVer, 49 | }, 50 | } 51 | } 52 | 53 | func getLatestAppVersion(llc client.Client, appType string) (appVer string, err error) { 54 | spec := (&api.AppVersionSpec{}).Init() 55 | if err = llc.CallApi(spec, context.Background()); err == nil { 56 | versionInfo := spec.Result[appType] 57 | appVer = versionInfo.VersionCode 58 | } 59 | return 60 | } 61 | -------------------------------------------------------------------------------- /dir.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "strings" 7 | 8 | "github.com/deadblue/elevengo/lowlevel/api" 9 | ) 10 | 11 | // DirMake makes directory under parentId, and returns its ID. 12 | func (a *Agent) DirMake(parentId string, name string) (dirId string, err error) { 13 | spec := (&api.DirCreateSpec{}).Init(parentId, name) 14 | if err = a.llc.CallApi(spec, context.Background()); err == nil { 15 | dirId = spec.Result 16 | } 17 | return 18 | } 19 | 20 | // DirSetOrder sets how files under the directory be ordered. 21 | func (a *Agent) DirSetOrder(dirId string, order FileOrder, asc bool) (err error) { 22 | spec := (&api.DirSetOrderSpec{}).Init( 23 | dirId, getOrderName(order), asc, 24 | ) 25 | return a.llc.CallApi(spec, context.Background()) 26 | } 27 | 28 | // DirGetId retrieves directory ID from full path. 29 | func (a *Agent) DirGetId(path string) (dirId string, err error) { 30 | path = strings.TrimPrefix(path, "/") 31 | spec := (&api.DirLocateSpec{}).Init(path) 32 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 33 | return 34 | } 35 | if spec.Result == "0" { 36 | err = os.ErrNotExist 37 | } else { 38 | dirId = spec.Result 39 | } 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package elevengo is an API client for 115 Cloud Storage Service, it bases on 2 | // the official APIs that are captures from Web and Desktop App. 3 | package elevengo 4 | -------------------------------------------------------------------------------- /download.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/deadblue/elevengo/internal/util" 8 | "github.com/deadblue/elevengo/lowlevel/api" 9 | "github.com/deadblue/elevengo/lowlevel/client" 10 | "github.com/deadblue/elevengo/lowlevel/errors" 11 | ) 12 | 13 | // DownloadTicket contains all required information to download a file. 14 | type DownloadTicket struct { 15 | // Download URL. 16 | Url string 17 | // Request headers which SHOULD be sent with download URL. 18 | Headers map[string]string 19 | // File name. 20 | FileName string 21 | // File size in bytes. 22 | FileSize int64 23 | } 24 | 25 | // DownloadCreateTicket creates ticket which contains all required information 26 | // to download a file. Caller can use third-party tools/libraries to download 27 | // file, such as wget/curl/aria2. 28 | func (a *Agent) DownloadCreateTicket(pickcode string, ticket *DownloadTicket) (err error) { 29 | // Prepare API spec. 30 | spec := (&api.DownloadSpec{}).Init(pickcode) 31 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 32 | return 33 | } 34 | // Convert result. 35 | if len(spec.Result) == 0 { 36 | return errors.ErrDownloadEmpty 37 | } 38 | for _, info := range spec.Result { 39 | ticket.FileSize, _ = info.FileSize.Int64() 40 | if ticket.FileSize == 0 { 41 | return errors.ErrDownloadDirectory 42 | } 43 | ticket.FileName = info.FileName 44 | ticket.Url = info.Url.Url 45 | ticket.Headers = map[string]string{ 46 | // User-Agent header 47 | "User-Agent": a.llc.GetUserAgent(), 48 | // Cookie header 49 | "Cookie": util.MarshalCookies(a.llc.ExportCookies(ticket.Url)), 50 | } 51 | break 52 | } 53 | return 54 | } 55 | 56 | // Fetch gets content from url using agent underlying HTTP client. 57 | func (a *Agent) Fetch(url string) (body client.Body, err error) { 58 | return a.llc.Get(url, nil, context.Background()) 59 | } 60 | 61 | // Range is used in Agent.GetRange(). 62 | type Range struct { 63 | start, end int64 64 | } 65 | 66 | func (r *Range) headerValue() string { 67 | // Generate Range header. 68 | // Reference: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range#syntax 69 | if r.start < 0 { 70 | return fmt.Sprintf("bytes=%d", r.start) 71 | } else { 72 | if r.end < 0 { 73 | return fmt.Sprintf("bytes=%d-", r.start) 74 | } else if r.end > r.start { 75 | return fmt.Sprintf("bytes=%d-%d", r.start, r.end) 76 | } 77 | } 78 | // (r.start >= 0 && r.end <= r.start) is an invalid range 79 | return "" 80 | } 81 | 82 | // RangeFirst makes a Range parameter to request the first `length` bytes. 83 | func RangeFirst(length int64) Range { 84 | return Range{ 85 | start: 0, 86 | end: length - 1, 87 | } 88 | } 89 | 90 | // RangeLast makes a Range parameter to request the last `length` bytes. 91 | func RangeLast(length int64) Range { 92 | return Range{ 93 | start: 0 - length, 94 | end: 0, 95 | } 96 | } 97 | 98 | // RangeMiddle makes a Range parameter to request content starts from `offset`, 99 | // and has `length` bytes (at most). 100 | // 101 | // You can pass a negative number in `length`, to request content starts from 102 | // `offset` to the end. 103 | func RangeMiddle(offset, length int64) Range { 104 | end := offset + length - 1 105 | if length < 0 { 106 | end = -1 107 | } 108 | return Range{ 109 | start: offset, 110 | end: end, 111 | } 112 | } 113 | 114 | // FetchRange gets partial content from |url|, which is located by |rng|. 115 | func (a *Agent) FetchRange(url string, rng Range) (body client.Body, err error) { 116 | headers := make(map[string]string) 117 | if value := rng.headerValue(); value != "" { 118 | headers["Range"] = value 119 | } 120 | return a.llc.Get(url, headers, context.Background()) 121 | } 122 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "os/exec" 8 | "path" 9 | "strings" 10 | 11 | "github.com/deadblue/elevengo/option" 12 | ) 13 | 14 | func ExampleAgent_CredentialImport() { 15 | agent := Default() 16 | 17 | // Import credential to agent 18 | if err := agent.CredentialImport(&Credential{ 19 | UID: "UID-From-Cookie", 20 | CID: "CID-From-Cookie", 21 | KID: "KID-From-Cookie", 22 | SEID: "SEID-From-Cookie", 23 | }); err != nil { 24 | log.Fatalf("Import credentail error: %s", err) 25 | } 26 | } 27 | 28 | func ExampleAgent_FileIterate() { 29 | agent := Default() 30 | 31 | it, err := agent.FileIterate("0") 32 | if err != nil { 33 | log.Fatalf("Iterate file failed: %s", err.Error()) 34 | } 35 | log.Printf("File count: %d", it.Count()) 36 | for index, file := range it.Items() { 37 | log.Printf("File: %d => %#v", index, file) 38 | } 39 | } 40 | 41 | func ExampleAgent_OfflineIterate() { 42 | agent := Default() 43 | it, err := agent.OfflineIterate() 44 | if err == nil { 45 | log.Printf("Task count: %d", it.Count()) 46 | for index, task := range it.Items() { 47 | log.Printf("Offline task: %d => %#v", index, task) 48 | } 49 | } else { 50 | log.Fatalf("Iterate offline task failed: %s", err) 51 | } 52 | } 53 | 54 | func ExampleAgent_DownloadCreateTicket() { 55 | agent := Default() 56 | 57 | // Create download ticket 58 | var err error 59 | ticket := DownloadTicket{} 60 | if err = agent.DownloadCreateTicket("pickcode", &ticket); err != nil { 61 | log.Fatalf("Get download ticket error: %s", err) 62 | } 63 | 64 | // Process download ticket through curl 65 | cmd := exec.Command("/usr/bin/curl", ticket.Url) 66 | for name, value := range ticket.Headers { 67 | cmd.Args = append(cmd.Args, "-H", fmt.Sprintf("%s: %s", name, value)) 68 | } 69 | cmd.Args = append(cmd.Args, "-o", ticket.FileName) 70 | if err = cmd.Run(); err != nil { 71 | log.Fatal(err) 72 | } else { 73 | log.Printf("File downloaded to %s", ticket.FileName) 74 | } 75 | } 76 | 77 | func ExampleAgent_UploadCreateTicket() { 78 | agent := Default() 79 | 80 | filename := "/path/to/file" 81 | file, err := os.Open(filename) 82 | if err != nil { 83 | log.Fatalf("Open file failed: %s", err.Error()) 84 | } 85 | 86 | ticket := &UploadTicket{} 87 | if err = agent.UploadCreateTicket("dirId", path.Base(filename), file, ticket); err != nil { 88 | log.Fatalf("Create upload ticket failed: %s", err.Error()) 89 | } 90 | if ticket.Exist { 91 | log.Printf("File already exists!") 92 | return 93 | } 94 | 95 | // Make temp file to receive upload result 96 | tmpFile, err := os.CreateTemp("", "curl-upload-*") 97 | if err != nil { 98 | log.Fatalf("Create temp file failed: %s", err) 99 | } 100 | defer func() { 101 | _ = tmpFile.Close() 102 | _ = os.Remove(tmpFile.Name()) 103 | }() 104 | 105 | // Use "curl" to upload file 106 | cmd := exec.Command("curl", ticket.Url, 107 | "-o", tmpFile.Name(), "-#", 108 | "-T", filename) 109 | for name, value := range ticket.Header { 110 | cmd.Args = append(cmd.Args, "-H", fmt.Sprintf("%s: %s", name, value)) 111 | } 112 | if err = cmd.Run(); err != nil { 113 | log.Fatalf("Upload failed: %s", err) 114 | } 115 | 116 | // Parse upload result 117 | uploadFile := &File{} 118 | if err = agent.UploadParseResult(tmpFile, uploadFile); err != nil { 119 | log.Fatalf("Parse upload result failed: %s", err) 120 | } else { 121 | log.Printf("Uploaded file: %#v", file) 122 | } 123 | } 124 | 125 | func ExampleAgent_VideoCreateTicket() { 126 | agent := Default() 127 | 128 | // Create video play ticket 129 | ticket := &VideoTicket{} 130 | err := agent.VideoCreateTicket("pickcode", ticket) 131 | if err != nil { 132 | log.Fatalf("Get video info failed: %s", err) 133 | } 134 | 135 | headers := make([]string, 0, len(ticket.Headers)) 136 | for name, value := range ticket.Headers { 137 | headers = append(headers, fmt.Sprintf("'%s: %s'", name, value)) 138 | } 139 | 140 | // Play HLS through mpv 141 | cmd := exec.Command("mpv") 142 | cmd.Args = append(cmd.Args, 143 | fmt.Sprintf("--http-header-fields=%s", strings.Join(headers, ",")), 144 | ticket.Url, 145 | ) 146 | cmd.Run() 147 | } 148 | 149 | func ExampleAgent_QrcodeStart() { 150 | agent := Default() 151 | 152 | session := &QrcodeSession{} 153 | err := agent.QrcodeStart(session) 154 | if err != nil { 155 | log.Fatalf("Start QRcode session error: %s", err) 156 | } 157 | // TODO: Show QRcode and ask user to scan it via 115 app. 158 | for done := false; !done && err != nil; { 159 | done, err = agent.QrcodePoll(session) 160 | } 161 | if err != nil { 162 | log.Fatalf("QRcode login failed, error: %s", err) 163 | } 164 | } 165 | 166 | func ExampleAgent_Import() { 167 | var err error 168 | 169 | // Initialize two agents for sender and receiver 170 | sender, receiver := Default(), Default() 171 | sender.CredentialImport(&Credential{ 172 | UID: "", CID: "", SEID: "", 173 | }) 174 | receiver.CredentialImport(&Credential{ 175 | UID: "", CID: "", SEID: "", 176 | }) 177 | 178 | // File to send on sender's storage 179 | fileId := "12345678" 180 | // Create import ticket by sender 181 | ticket, pickcode := &ImportTicket{}, "" 182 | if pickcode, err = sender.ImportCreateTicket(fileId, ticket); err != nil { 183 | log.Fatalf("Get file info failed: %s", err) 184 | } 185 | 186 | // Directory to save file on receiver's storage 187 | dirId := "0" 188 | // Call Import first time 189 | if err = receiver.Import(dirId, ticket); err != nil { 190 | if ie, ok := err.(*ErrImportNeedCheck); ok { 191 | // Calculate sign value by sender 192 | signValue, err := sender.ImportCalculateSignValue(pickcode, ie.SignRange) 193 | if err != nil { 194 | log.Fatalf("Calculate sign value failed: %s", err) 195 | } 196 | // Update ticket and import again 197 | ticket.SignKey, ticket.SignValue = ie.SignKey, signValue 198 | if err = receiver.Import(dirId, ticket); err == nil { 199 | log.Print("Import succeeded!") 200 | } else { 201 | log.Fatalf("Import failed: %s", err) 202 | } 203 | } else { 204 | log.Fatalf("Import failed: %s", err) 205 | } 206 | } else { 207 | log.Print("Import succeeded!") 208 | } 209 | } 210 | 211 | func ExampleNew() { 212 | // Customize agent 213 | agent := New( 214 | option.Agent().WithName("Evangelion/1.0").WithCooldown(100, 500), 215 | ) 216 | 217 | var err error 218 | if err = agent.CredentialImport(&Credential{ 219 | UID: "", CID: "", SEID: "", 220 | }); err != nil { 221 | log.Fatalf("Invalid credential, error: %s", err) 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /file.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | "time" 7 | 8 | "github.com/deadblue/elevengo/internal/protocol" 9 | "github.com/deadblue/elevengo/internal/util" 10 | "github.com/deadblue/elevengo/lowlevel/api" 11 | "github.com/deadblue/elevengo/lowlevel/client" 12 | "github.com/deadblue/elevengo/lowlevel/errors" 13 | "github.com/deadblue/elevengo/lowlevel/types" 14 | "github.com/deadblue/elevengo/option" 15 | ) 16 | 17 | // File describe a file or directory on cloud storage. 18 | type File struct { 19 | // Marks is the file a directory. 20 | IsDirectory bool 21 | 22 | // Unique identifier of the file on the cloud storage. 23 | FileId string 24 | // FileId of the parent directory. 25 | ParentId string 26 | 27 | // Base name of the file. 28 | Name string 29 | // Size in bytes of the file. 30 | Size int64 31 | // Identifier used for downloading or playing the file. 32 | PickCode string 33 | // SHA1 hash of file content, in HEX format. 34 | Sha1 string 35 | 36 | // Is file stared 37 | Star bool 38 | // File labels 39 | Labels []Label 40 | 41 | // Last modified time 42 | ModifiedTime time.Time 43 | 44 | // Play duration in seconds, for audio/video files. 45 | MediaDuration float64 46 | // Is file a video. 47 | IsVideo bool 48 | // Definition of the video file. 49 | VideoDefinition VideoDefinition 50 | } 51 | 52 | func (f *File) from(info *types.FileInfo) *File { 53 | if info.FileId != "" { 54 | f.FileId = info.FileId 55 | f.ParentId = info.CategoryId 56 | f.IsDirectory = false 57 | } else { 58 | f.FileId = info.CategoryId 59 | f.ParentId = info.ParentId 60 | f.IsDirectory = true 61 | } 62 | f.Name = info.Name 63 | f.Size = info.Size.Int64() 64 | f.PickCode = info.PickCode 65 | f.Sha1 = info.Sha1 66 | 67 | f.Star = bool(info.IsStar) 68 | f.Labels = make([]Label, len(info.Labels)) 69 | for i, l := range info.Labels { 70 | f.Labels[i].Id = l.Id 71 | f.Labels[i].Name = l.Name 72 | f.Labels[i].Color = labelColorRevMap[l.Color] 73 | } 74 | 75 | if info.UpdatedTime != "" { 76 | f.ModifiedTime = util.ParseFileTime(info.UpdatedTime) 77 | } else { 78 | f.ModifiedTime = util.ParseFileTime(info.ModifiedTime) 79 | } 80 | 81 | f.MediaDuration = info.MediaDuration 82 | f.IsVideo = info.VideoFlag == 1 83 | if f.IsVideo { 84 | f.VideoDefinition = VideoDefinition(info.VideoDefinition) 85 | } 86 | 87 | return f 88 | } 89 | 90 | type fileIterator struct { 91 | llc client.Client 92 | 93 | // Offset & limit 94 | offset, limit int 95 | // Root directory ID 96 | dirId string 97 | // Sort order 98 | order string 99 | // Is ascending? 100 | asc int 101 | // File type 102 | type_ int 103 | // File extension 104 | ext string 105 | 106 | // Iterate mode: 107 | // - 1: list 108 | // - 2: star 109 | // - 3: search 110 | // - 4: label 111 | mode int 112 | // Parameters for mode=3 113 | keyword string 114 | // Parameters for mode=4 115 | labelId string 116 | 117 | result *types.FileListResult 118 | } 119 | 120 | func (i *fileIterator) updateList() (err error) { 121 | if i.result != nil && i.offset >= i.result.Count { 122 | return errNoMoreItems 123 | } 124 | spec := (&api.FileListSpec{}).Init(i.dirId, i.offset, i.limit) 125 | if i.mode == 2 { 126 | spec.SetStared() 127 | } else { 128 | if i.order == "" { 129 | i.order = api.FileOrderDefault 130 | } 131 | spec.SetOrder(i.order, i.asc) 132 | } 133 | if i.type_ >= 0 { 134 | spec.SetFileType(i.type_) 135 | } else { 136 | spec.SetFileExtension(i.ext) 137 | } 138 | for { 139 | if err = i.llc.CallApi(spec, context.Background()); err == nil { 140 | break 141 | } 142 | if ferr, ok := err.(*errors.FileOrderInvalidError); ok { 143 | spec.SetOrder(ferr.Order, ferr.Asc) 144 | } else { 145 | return 146 | } 147 | } 148 | i.result = &spec.Result 149 | i.order, i.asc = spec.Result.Order, spec.Result.Asc 150 | return 151 | } 152 | 153 | func (i *fileIterator) updateSearch() (err error) { 154 | if i.result != nil && i.offset >= i.result.Count { 155 | return errNoMoreItems 156 | } 157 | spec := (&api.FileSearchSpec{}).Init(i.offset, i.limit) 158 | if i.type_ >= 0 { 159 | spec.SetFileType(i.type_) 160 | } else { 161 | spec.SetFileExtension(i.ext) 162 | } 163 | switch i.mode { 164 | case 3: 165 | spec.ByKeyword(i.dirId, i.keyword) 166 | case 4: 167 | spec.ByLabelId(i.labelId) 168 | } 169 | if err = i.llc.CallApi(spec, context.Background()); err == nil { 170 | i.result = &spec.Result 171 | } 172 | return 173 | } 174 | 175 | func (i *fileIterator) update() (err error) { 176 | if i.dirId == "" { 177 | i.dirId = "" 178 | } 179 | if i.limit == 0 { 180 | i.limit = protocol.FileListLimit 181 | } 182 | switch i.mode { 183 | case 1, 2: 184 | err = i.updateList() 185 | case 3, 4: 186 | err = i.updateSearch() 187 | } 188 | return 189 | } 190 | 191 | func (i *fileIterator) Count() int { 192 | if i.result == nil { 193 | return 0 194 | } 195 | return i.result.Count 196 | } 197 | 198 | func (i *fileIterator) Items() iter.Seq2[int, *File] { 199 | return func(yield func(int, *File) bool) { 200 | for { 201 | for index, fi := range i.result.Files { 202 | if cont := yield(i.offset+index, (&File{}).from(fi)); !cont { 203 | return 204 | } 205 | } 206 | i.offset += i.limit 207 | if err := i.update(); err != nil { 208 | break 209 | } 210 | } 211 | } 212 | } 213 | 214 | // FileIterate list files under directory, whose id is |dirId|. 215 | func (a *Agent) FileIterate(dirId string) (it Iterator[File], err error) { 216 | fi := &fileIterator{ 217 | llc: a.llc, 218 | dirId: dirId, 219 | mode: 1, 220 | } 221 | if err = fi.update(); err == nil { 222 | it = fi 223 | } 224 | return 225 | } 226 | 227 | // FileWithStar lists files with star. 228 | func (a *Agent) FileWithStar(options ...*option.FileListOptions) (it Iterator[File], err error) { 229 | fi := &fileIterator{ 230 | llc: a.llc, 231 | mode: 2, 232 | } 233 | // Apply options 234 | if opts := util.NotNull(options...); opts != nil { 235 | fi.type_ = opts.Type 236 | fi.ext = opts.ExtName 237 | } 238 | if err = fi.update(); err == nil { 239 | it = fi 240 | } 241 | return 242 | } 243 | 244 | // FileSearch recursively searches files under a directory, whose name contains 245 | // the given keyword. 246 | func (a *Agent) FileSearch( 247 | dirId, keyword string, options ...*option.FileListOptions, 248 | ) (it Iterator[File], err error) { 249 | fi := &fileIterator{ 250 | llc: a.llc, 251 | dirId: dirId, 252 | mode: 3, 253 | keyword: keyword, 254 | } 255 | // Apply options 256 | if opts := util.NotNull(options...); opts != nil { 257 | fi.type_ = opts.Type 258 | fi.ext = opts.ExtName 259 | } 260 | if err = fi.update(); err == nil { 261 | it = fi 262 | } 263 | return 264 | } 265 | 266 | // FileLabeled lists files which has specific label. 267 | func (a *Agent) FileWithLabel( 268 | labelId string, options ...*option.FileListOptions, 269 | ) (it Iterator[File], err error) { 270 | fi := &fileIterator{ 271 | llc: a.llc, 272 | mode: 4, 273 | labelId: labelId, 274 | } 275 | // Apply options 276 | if opts := util.NotNull(options...); opts != nil { 277 | fi.type_ = opts.Type 278 | fi.ext = opts.ExtName 279 | } 280 | if err = fi.update(); err == nil { 281 | it = fi 282 | } 283 | return 284 | } 285 | 286 | // FileGet gets information of a file/directory by its ID. 287 | func (a *Agent) FileGet(fileId string, file *File) (err error) { 288 | spec := (&api.FileGetSpec{}).Init(fileId) 289 | if err = a.llc.CallApi(spec, context.Background()); err == nil { 290 | file.from(spec.Result[0]) 291 | } 292 | return 293 | } 294 | 295 | // FileMove moves files into target directory whose id is dirId. 296 | func (a *Agent) FileMove(dirId string, fileIds []string) (err error) { 297 | if len(fileIds) == 0 { 298 | return 299 | } 300 | spec := (&api.FileMoveSpec{}).Init(dirId, fileIds) 301 | return a.llc.CallApi(spec, context.Background()) 302 | } 303 | 304 | // FileCopy copies files into target directory whose id is dirId. 305 | func (a *Agent) FileCopy(dirId string, fileIds []string) (err error) { 306 | if len(fileIds) == 0 { 307 | return 308 | } 309 | spec := (&api.FileCopySpec{}).Init(dirId, fileIds) 310 | return a.llc.CallApi(spec, context.Background()) 311 | } 312 | 313 | // FileRename renames file to new name. 314 | func (a *Agent) FileRename(fileId, newName string) (err error) { 315 | if fileId == "" || newName == "" { 316 | return 317 | } 318 | spec := (&api.FileRenameSpec{}).Init() 319 | spec.Add(fileId, newName) 320 | return a.llc.CallApi(spec, context.Background()) 321 | } 322 | 323 | // FileBatchRename renames multiple files. 324 | func (a *Agent) FileBatchRename(nameMap map[string]string) (err error) { 325 | spec := (&api.FileRenameSpec{}).Init() 326 | for fileId, newName := range nameMap { 327 | if fileId == "" || newName == "" { 328 | continue 329 | } 330 | spec.Add(fileId, newName) 331 | } 332 | return a.llc.CallApi(spec, context.Background()) 333 | } 334 | 335 | // FileDelete deletes files. 336 | func (a *Agent) FileDelete(fileIds []string) (err error) { 337 | if len(fileIds) == 0 { 338 | return 339 | } 340 | spec := (&api.FileDeleteSpec{}).Init(fileIds) 341 | return a.llc.CallApi(spec, context.Background()) 342 | } 343 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/deadblue/elevengo 2 | 3 | require filippo.io/nistec v0.0.3 4 | 5 | go 1.23 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | filippo.io/nistec v0.0.3 h1:h336Je2jRDZdBCLy2fLDUd9E2unG32JLwcJi0JQE9Cw= 2 | filippo.io/nistec v0.0.3/go.mod h1:84fxC9mi+MhC2AERXI4LSa8cmSVOzrFikg6hZ4IfCyw= 3 | -------------------------------------------------------------------------------- /import.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "crypto/sha1" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/deadblue/elevengo/internal/crypto/hash" 10 | "github.com/deadblue/elevengo/internal/util" 11 | "github.com/deadblue/elevengo/lowlevel/api" 12 | "github.com/deadblue/elevengo/lowlevel/errors" 13 | ) 14 | 15 | type ErrImportNeedCheck struct { 16 | // The sign key your should set to ImportTicket 17 | SignKey string 18 | // The sign range in format of "-" in bytes. 19 | // You can directly use it in ImportCreateTicket. 20 | SignRange string 21 | } 22 | 23 | func (e *ErrImportNeedCheck) Error() string { 24 | return "import requires sign check" 25 | } 26 | 27 | // ImportTicket container reqiured fields to import(aka. quickly upload) a file 28 | // to your 115 cloud storage. 29 | type ImportTicket struct { 30 | // File base name 31 | FileName string 32 | // File size in bytes 33 | FileSize int64 34 | // File SHA-1 hash, in upper-case HEX format 35 | FileSha1 string 36 | // Sign key from 115 server. 37 | SignKey string 38 | // SHA-1 hash value of a segment of the file, in upper-case HEX format 39 | SignValue string 40 | } 41 | 42 | // Import imports(aka. rapid-upload) a file to your 115 cloud storage. 43 | // Please check example code for the detailed usage. 44 | func (a *Agent) Import(dirId string, ticket *ImportTicket) (err error) { 45 | spec := (&api.UploadInitSpec{}).Init( 46 | dirId, ticket.FileSha1, ticket.FileName, ticket.FileSize, 47 | ticket.SignKey, ticket.SignValue, &a.common, 48 | ) 49 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 50 | return 51 | } 52 | if spec.Result.SignCheck != "" { 53 | err = &ErrImportNeedCheck{ 54 | SignKey: spec.Result.SignKey, 55 | SignRange: spec.Result.SignCheck, 56 | } 57 | } else if !spec.Result.Exists { 58 | err = errors.ErrNotExist 59 | } 60 | return 61 | } 62 | 63 | // ImportCreateTicket is a helper function to create an ImportTicket of a file, 64 | // that you can share to others to import this file to their cloud storage. 65 | // You should also send pickcode together with ticket. 66 | func (a *Agent) ImportCreateTicket(fileId string, ticket *ImportTicket) (pickcode string, err error) { 67 | file := &File{} 68 | if err = a.FileGet(fileId, file); err == nil { 69 | pickcode = file.PickCode 70 | if ticket != nil { 71 | ticket.FileName = file.Name 72 | ticket.FileSize = file.Size 73 | ticket.FileSha1 = file.Sha1 74 | } 75 | } 76 | return 77 | } 78 | 79 | // ImportCalculateSignValue calculates sign value of a file on cloud storage. 80 | // Please check example code for the detailed usage. 81 | func (a *Agent) ImportCalculateSignValue(pickcode string, signRange string) (value string, err error) { 82 | // Parse range text at first 83 | var start, end int64 84 | if _, err = fmt.Sscanf(signRange, "%d-%d", &start, &end); err != nil { 85 | return 86 | } 87 | // Get download URL 88 | ticket := &DownloadTicket{} 89 | if err = a.DownloadCreateTicket(pickcode, ticket); err != nil { 90 | return 91 | } 92 | // Get range content 93 | var body io.ReadCloser 94 | if body, err = a.FetchRange(ticket.Url, Range{start, end}); err != nil { 95 | return 96 | } 97 | defer util.QuietlyClose(body) 98 | h := sha1.New() 99 | if _, err = io.Copy(h, body); err == nil { 100 | value = hash.ToHexUpper(h) 101 | } 102 | return 103 | } 104 | -------------------------------------------------------------------------------- /internal/crypto/ec115/api.go: -------------------------------------------------------------------------------- 1 | package ec115 2 | 3 | import ( 4 | "crypto/aes" 5 | "crypto/cipher" 6 | "encoding/base64" 7 | "encoding/binary" 8 | "errors" 9 | "hash/crc32" 10 | "math/rand" 11 | 12 | "github.com/deadblue/elevengo/internal/crypto/lz4" 13 | ) 14 | 15 | var ( 16 | crcSalt = []byte("^j>WD3Kr?J2gLFjD4W2y@") 17 | 18 | errInvalidEncodedData = errors.New("invalid ec data") 19 | ) 20 | 21 | func (c *Cipher) EncodeToken(timestamp int64) string { 22 | buf := make([]byte, 48) 23 | // Put information to buf 24 | copy(buf[0:15], c.pubKey[:15]) 25 | copy(buf[24:39], c.pubKey[15:]) 26 | buf[16], buf[40] = 115, 1 27 | binary.LittleEndian.PutUint32(buf[20:], uint32(timestamp)) 28 | // Encode it 29 | r1, r2 := byte(rand.Intn(0xff)), byte(rand.Intn(0xff)) 30 | for i := 0; i < 44; i++ { 31 | if i < 24 { 32 | buf[i] ^= r1 33 | } else { 34 | buf[i] ^= r2 35 | } 36 | } 37 | // Calculate checksum 38 | h := crc32.NewIEEE() 39 | h.Write(crcSalt) 40 | h.Write(buf[:44]) 41 | // Save checksum at the end 42 | binary.LittleEndian.PutUint32(buf[44:], h.Sum32()) 43 | return base64.StdEncoding.EncodeToString(buf) 44 | } 45 | 46 | func (c *Cipher) Encode(input []byte) (output []byte) { 47 | // Zero padding 48 | plaintext, plainSize := input, len(input) 49 | if padSize := aes.BlockSize - (plainSize % aes.BlockSize); padSize != aes.BlockSize { 50 | plaintext = make([]byte, plainSize+padSize) 51 | copy(plaintext, input) 52 | // Make sure all padding bytes are zero 53 | for i := 0; i < padSize; i++ { 54 | plaintext[plainSize+i] = 0 55 | } 56 | plainSize += padSize 57 | } 58 | // Initialize encrypter 59 | block, _ := aes.NewCipher(c.aesKey) 60 | enc := cipher.NewCBCEncrypter(block, c.aesIv) 61 | // Encrypt 62 | output = make([]byte, plainSize) 63 | enc.CryptBlocks(output, plaintext) 64 | return 65 | } 66 | 67 | func (c *Cipher) Decode(input []byte) (output []byte, err error) { 68 | cryptoSize := len(input) - 12 69 | if cryptoSize < 12 { 70 | return nil, errInvalidEncodedData 71 | } 72 | cryptotext, tail := input[:cryptoSize], input[cryptoSize:] 73 | // Validate input data 74 | h := crc32.NewIEEE() 75 | h.Write(crcSalt) 76 | h.Write(tail[0:8]) 77 | if h.Sum32() != binary.LittleEndian.Uint32(tail[8:12]) { 78 | return nil, errInvalidEncodedData 79 | } 80 | // Get output size 81 | for i := 0; i < 4; i++ { 82 | tail[i] ^= tail[7] 83 | } 84 | outputSize := binary.LittleEndian.Uint32(tail[0:4]) 85 | output = make([]byte, outputSize) 86 | // Initialize decrypter 87 | block, _ := aes.NewCipher(c.aesKey) 88 | dec := cipher.NewCBCDecrypter(block, c.aesIv) 89 | // Decrypt 90 | plaintext := make([]byte, cryptoSize) 91 | dec.CryptBlocks(plaintext, cryptotext) 92 | // Uncompress 93 | for buf := output; err == nil && outputSize > 0; { 94 | // Each block is 8192 bytes at maximum 95 | bufSize := outputSize 96 | if bufSize > 8192 { 97 | bufSize = 8192 98 | } 99 | srcSize := binary.LittleEndian.Uint16(plaintext[0:2]) 100 | err = lz4.BlockUncompress(plaintext[2:srcSize+2], buf) 101 | // Prepare for next block 102 | plaintext = plaintext[srcSize+2:] 103 | buf = buf[bufSize:] 104 | outputSize -= bufSize 105 | } 106 | return 107 | } 108 | -------------------------------------------------------------------------------- /internal/crypto/ec115/cipher.go: -------------------------------------------------------------------------------- 1 | package ec115 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | 8 | "filippo.io/nistec" 9 | ) 10 | 11 | var ( 12 | serverKey = []byte{ 13 | 0x04, 0x57, 0xa2, 0x92, 0x57, 0xcd, 0x23, 0x20, 14 | 0xe5, 0xd6, 0xd1, 0x43, 0x32, 0x2f, 0xa4, 0xbb, 15 | 0x8a, 0x3c, 0xf9, 0xd3, 0xcc, 0x62, 0x3e, 0xf5, 16 | 0xed, 0xac, 0x62, 0xb7, 0x67, 0x8a, 0x89, 0xc9, 17 | 0x1a, 0x83, 0xba, 0x80, 0x0d, 0x61, 0x29, 0xf5, 18 | 0x22, 0xd0, 0x34, 0xc8, 0x95, 0xdd, 0x24, 0x65, 19 | 0x24, 0x3a, 0xdd, 0xc2, 0x50, 0x95, 0x3b, 0xee, 20 | 0xba, 21 | } 22 | ) 23 | 24 | type Cipher struct { 25 | // Client public key 26 | pubKey []byte 27 | // AES key & IV 28 | aesKey []byte 29 | aesIv []byte 30 | } 31 | 32 | func New() *Cipher { 33 | curve := elliptic.P224() 34 | // Generate local key 35 | localKey, _ := ecdsa.GenerateKey(curve, rand.Reader) 36 | scalar := make([]byte, (curve.Params().BitSize+7)/8) 37 | localKey.D.FillBytes(scalar) 38 | pubKey := elliptic.MarshalCompressed(curve, localKey.X, localKey.Y) 39 | // Parse remote key 40 | remoteKey, _ := nistec.NewP224Point().SetBytes(serverKey) 41 | // ECDH key exchange 42 | sharedPoint, _ := nistec.NewP224Point().ScalarMult(remoteKey, scalar) 43 | sharedSecret, _ := sharedPoint.BytesX() 44 | // Instantiate cipher 45 | pubKeySize, sharedSecretSize := len(pubKey), len(sharedSecret) 46 | cipher := &Cipher{ 47 | pubKey: make([]byte, pubKeySize+1), 48 | aesKey: make([]byte, 16), 49 | aesIv: make([]byte, 16), 50 | } 51 | cipher.pubKey[0] = byte(pubKeySize) 52 | copy(cipher.pubKey[1:], pubKey) 53 | copy(cipher.aesKey, sharedSecret[:16]) 54 | copy(cipher.aesIv, sharedSecret[sharedSecretSize-16:]) 55 | return cipher 56 | } 57 | -------------------------------------------------------------------------------- /internal/crypto/hash/digest.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/md5" 5 | "crypto/sha1" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | type DigestResult struct { 11 | Size int64 12 | SHA1 string 13 | MD5 string 14 | } 15 | 16 | func Digest(r io.Reader, result *DigestResult) (err error) { 17 | hs, hm := sha1.New(), md5.New() 18 | w := io.MultiWriter(hs, hm) 19 | // Write remain data. 20 | if result.Size, err = io.Copy(w, r); err != nil { 21 | return 22 | } 23 | result.SHA1, result.MD5 = ToHexUpper(hs), ToBase64(hm) 24 | return nil 25 | } 26 | 27 | func DigestRange(r io.ReadSeeker, rangeSpec string) (result string, err error) { 28 | var start, end int64 29 | if _, err = fmt.Sscanf(rangeSpec, "%d-%d", &start, &end); err != nil { 30 | return 31 | } 32 | h := sha1.New() 33 | r.Seek(start, io.SeekStart) 34 | if _, err = io.CopyN(h, r, end-start+1); err == nil { 35 | result = ToHexUpper(h) 36 | } 37 | return 38 | } 39 | -------------------------------------------------------------------------------- /internal/crypto/hash/encode.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/hex" 6 | "hash" 7 | "strings" 8 | ) 9 | 10 | func ToHex(h hash.Hash) string { 11 | return hex.EncodeToString(h.Sum(nil)) 12 | } 13 | 14 | func ToHexUpper(h hash.Hash) string { 15 | return strings.ToUpper(ToHex(h)) 16 | } 17 | 18 | func ToBase64(h hash.Hash) string { 19 | return base64.StdEncoding.EncodeToString(h.Sum(nil)) 20 | } 21 | -------------------------------------------------------------------------------- /internal/crypto/hash/short.go: -------------------------------------------------------------------------------- 1 | package hash 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | ) 7 | 8 | func Md5Hex(str string) string { 9 | h := md5.Sum([]byte(str)) 10 | return hex.EncodeToString(h[:]) 11 | } 12 | -------------------------------------------------------------------------------- /internal/crypto/lz4/block.go: -------------------------------------------------------------------------------- 1 | package lz4 2 | 3 | // BlockUncompress uncompresses a lz4 block. 4 | func BlockUncompress(src, dst []byte) (err error) { 5 | srcBuf, dstBuf := newBuffer(src), newBuffer(dst) 6 | var literalLen, matchLen, extraLen, offset int 7 | for err == nil { 8 | // Read token 9 | if literalLen, matchLen, err = srcBuf.ReadToken(); err != nil { 10 | break 11 | } 12 | // Optional: Copy literal to dst 13 | if literalLen > 0 { 14 | if literalLen == 0x0f { 15 | if extraLen, err = srcBuf.ReadExtraLength(); err != nil { 16 | break 17 | } 18 | literalLen += extraLen 19 | } 20 | if err = CopyN(dstBuf, srcBuf, literalLen); err != nil { 21 | break 22 | } 23 | } 24 | // Early return when reach the end 25 | if srcBuf.AtTheEnd() { 26 | break 27 | } 28 | // Read offset 29 | if offset, err = srcBuf.ReadOffset(); err != nil { 30 | break 31 | } 32 | // Update match length 33 | if matchLen == 0x13 { 34 | if extraLen, err = srcBuf.ReadExtraLength(); err != nil { 35 | break 36 | } 37 | matchLen += extraLen 38 | } 39 | dstBuf.WriteMatchedLiteral(offset, matchLen) 40 | } 41 | return 42 | } 43 | -------------------------------------------------------------------------------- /internal/crypto/lz4/buffer.go: -------------------------------------------------------------------------------- 1 | package lz4 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ( 8 | errOutOfRange = errors.New("out of range") 9 | ) 10 | 11 | type Buffer struct { 12 | buf []byte 13 | cursor int 14 | size int 15 | } 16 | 17 | func newBuffer(p []byte) *Buffer { 18 | return &Buffer{ 19 | buf: p, 20 | cursor: 0, 21 | size: len(p), 22 | } 23 | } 24 | 25 | func (b *Buffer) AtTheEnd() bool { 26 | return b.cursor == b.size 27 | } 28 | 29 | // Next return a slice of underlying buf for reading/writing. 30 | func (b *Buffer) Next(n int) (p []byte, err error) { 31 | if b.cursor+n > b.size { 32 | err = errOutOfRange 33 | } else { 34 | p = b.buf[b.cursor : b.cursor+n] 35 | b.cursor += n 36 | } 37 | return 38 | } 39 | 40 | func (b *Buffer) ReadByte() (v byte, err error) { 41 | if b.cursor == b.size { 42 | err = errOutOfRange 43 | } else { 44 | v = b.buf[b.cursor] 45 | b.cursor += 1 46 | } 47 | return 48 | } 49 | 50 | func (b *Buffer) ReadToken() (literalLen, matchLen int, err error) { 51 | var v byte 52 | if v, err = b.ReadByte(); err == nil { 53 | literalLen = int(v >> 4) 54 | matchLen = 4 + int(v&0x0f) 55 | } 56 | return 57 | } 58 | 59 | func (b *Buffer) ReadExtraLength() (l int, err error) { 60 | var v byte 61 | for { 62 | if v, err = b.ReadByte(); err == nil { 63 | l += int(v) 64 | } 65 | if err != nil || v != 0xff { 66 | break 67 | } 68 | } 69 | return 70 | } 71 | 72 | func (b *Buffer) ReadOffset() (offset int, err error) { 73 | var p []byte 74 | if p, err = b.Next(2); err == nil { 75 | offset = int(p[0]) | (int(p[1]) << 8) 76 | } 77 | return 78 | } 79 | 80 | func (b *Buffer) WriteMatchedLiteral(offset, length int) (err error) { 81 | start := b.cursor - offset 82 | var p []byte 83 | if p, err = b.Next(length); err != nil { 84 | return 85 | } 86 | copy(p, b.buf[start:]) 87 | return 88 | } 89 | 90 | func CopyN(dst, src *Buffer, n int) (err error) { 91 | var sp, dp []byte 92 | if sp, err = src.Next(n); err != nil { 93 | return 94 | } 95 | if dp, err = dst.Next(n); err != nil { 96 | return 97 | } 98 | copy(dp, sp) 99 | return 100 | } 101 | -------------------------------------------------------------------------------- /internal/crypto/m115/public.go: -------------------------------------------------------------------------------- 1 | package m115 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "io" 7 | ) 8 | 9 | type Key [16]byte 10 | 11 | func GenerateKey() Key { 12 | key := Key{} 13 | _, _ = io.ReadFull(rand.Reader, key[:]) 14 | return key 15 | } 16 | 17 | func Encode(input []byte, key Key) (output string) { 18 | // Prepare buffer 19 | buf := make([]byte, 16+len(input)) 20 | // Copy key and data to buffer 21 | copy(buf, key[:]) 22 | copy(buf[16:], input) 23 | // XOR encode 24 | xorTransform(buf[16:], xorDeriveKey(key[:], 4)) 25 | reverseBytes(buf[16:]) 26 | xorTransform(buf[16:], xorClientKey) 27 | // Encrypt and encode 28 | output = base64.StdEncoding.EncodeToString(rsaEncrypt(buf)) 29 | return 30 | } 31 | 32 | func Decode(input string, key Key) (output []byte, err error) { 33 | // Base64 decode 34 | data, err := base64.StdEncoding.DecodeString(input) 35 | if err != nil { 36 | return 37 | } 38 | // RSA decrypt 39 | data = rsaDecrypt(data) 40 | // XOR decode 41 | output = make([]byte, len(data)-16) 42 | copy(output, data[16:]) 43 | xorTransform(output, xorDeriveKey(data[:16], 12)) 44 | reverseBytes(output) 45 | xorTransform(output, xorDeriveKey(key[:], 4)) 46 | return 47 | } 48 | -------------------------------------------------------------------------------- /internal/crypto/m115/rsa.go: -------------------------------------------------------------------------------- 1 | package m115 2 | 3 | import ( 4 | "bytes" 5 | "crypto/rand" 6 | "io" 7 | "math/big" 8 | ) 9 | 10 | var ( 11 | _N, _ = big.NewInt(0).SetString( 12 | "8686980c0f5a24c4b9d43020cd2c22703ff3f450756529058b1cf88f09b86021"+ 13 | "36477198a6e2683149659bd122c33592fdb5ad47944ad1ea4d36c6b172aad633"+ 14 | "8c3bb6ac6227502d010993ac967d1aef00f0c8e038de2e4d3bc2ec368af2e9f1"+ 15 | "0a6f1eda4f7262f136420c07c331b871bf139f74f3010e3c4fe57df3afb71683", 16) 16 | _E, _ = big.NewInt(0).SetString("10001", 16) 17 | 18 | _KeyLength = _N.BitLen() / 8 19 | ) 20 | 21 | func rsaEncrypt(input []byte) []byte { 22 | buf := &bytes.Buffer{} 23 | for remainSize := len(input); remainSize > 0; { 24 | sliceSize := _KeyLength - 11 25 | if sliceSize > remainSize { 26 | sliceSize = remainSize 27 | } 28 | rsaEncryptSlice(input[:sliceSize], buf) 29 | 30 | input = input[sliceSize:] 31 | remainSize -= sliceSize 32 | } 33 | return buf.Bytes() 34 | } 35 | 36 | func rsaEncryptSlice(input []byte, w io.Writer) { 37 | // Padding 38 | padSize := _KeyLength - len(input) - 3 39 | padData := make([]byte, padSize) 40 | _, _ = rand.Read(padData) 41 | // Prepare message 42 | buf := make([]byte, _KeyLength) 43 | buf[0], buf[1] = 0, 2 44 | for i, b := range padData { 45 | buf[2+i] = b%0xff + 0x01 46 | } 47 | buf[padSize+2] = 0 48 | copy(buf[padSize+3:], input) 49 | msg := big.NewInt(0).SetBytes(buf) 50 | // RSA Encrypt 51 | ret := big.NewInt(0).Exp(msg, _E, _N).Bytes() 52 | // Fill zeros at beginning 53 | if fillSize := _KeyLength - len(ret); fillSize > 0 { 54 | zeros := make([]byte, fillSize) 55 | _, _ = w.Write(zeros) 56 | } 57 | _, _ = w.Write(ret) 58 | } 59 | 60 | func rsaDecrypt(input []byte) []byte { 61 | buf := &bytes.Buffer{} 62 | for remainSize := len(input); remainSize > 0; { 63 | sliceSize := _KeyLength 64 | if sliceSize > remainSize { 65 | sliceSize = remainSize 66 | } 67 | rsaDecryptSlice(input[:sliceSize], buf) 68 | 69 | input = input[sliceSize:] 70 | remainSize -= sliceSize 71 | } 72 | return buf.Bytes() 73 | } 74 | 75 | func rsaDecryptSlice(input []byte, w io.Writer) { 76 | // RSA Decrypt 77 | msg := big.NewInt(0).SetBytes(input) 78 | ret := big.NewInt(0).Exp(msg, _E, _N).Bytes() 79 | // Un-padding 80 | for i, b := range ret { 81 | // Find the beginning of plaintext 82 | if b == 0 && i != 0 { 83 | _, _ = w.Write(ret[i+1:]) 84 | break 85 | } 86 | } 87 | return 88 | } 89 | -------------------------------------------------------------------------------- /internal/crypto/m115/util.go: -------------------------------------------------------------------------------- 1 | package m115 2 | 3 | // reverseBytes reverses data in place. 4 | func reverseBytes(data []byte) { 5 | for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 { 6 | data[i], data[j] = data[j], data[i] 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /internal/crypto/m115/xor.go: -------------------------------------------------------------------------------- 1 | package m115 2 | 3 | var ( 4 | // Pre-calculated key data 5 | xorKeySeed = []byte{ 6 | 0xf0, 0xe5, 0x69, 0xae, 0xbf, 0xdc, 0xbf, 0x8a, 7 | 0x1a, 0x45, 0xe8, 0xbe, 0x7d, 0xa6, 0x73, 0xb8, 8 | 0xde, 0x8f, 0xe7, 0xc4, 0x45, 0xda, 0x86, 0xc4, 9 | 0x9b, 0x64, 0x8b, 0x14, 0x6a, 0xb4, 0xf1, 0xaa, 10 | 0x38, 0x01, 0x35, 0x9e, 0x26, 0x69, 0x2c, 0x86, 11 | 0x00, 0x6b, 0x4f, 0xa5, 0x36, 0x34, 0x62, 0xa6, 12 | 0x2a, 0x96, 0x68, 0x18, 0xf2, 0x4a, 0xfd, 0xbd, 13 | 0x6b, 0x97, 0x8f, 0x4d, 0x8f, 0x89, 0x13, 0xb7, 14 | 0x6c, 0x8e, 0x93, 0xed, 0x0e, 0x0d, 0x48, 0x3e, 15 | 0xd7, 0x2f, 0x88, 0xd8, 0xfe, 0xfe, 0x7e, 0x86, 16 | 0x50, 0x95, 0x4f, 0xd1, 0xeb, 0x83, 0x26, 0x34, 17 | 0xdb, 0x66, 0x7b, 0x9c, 0x7e, 0x9d, 0x7a, 0x81, 18 | 0x32, 0xea, 0xb6, 0x33, 0xde, 0x3a, 0xa9, 0x59, 19 | 0x34, 0x66, 0x3b, 0xaa, 0xba, 0x81, 0x60, 0x48, 20 | 0xb9, 0xd5, 0x81, 0x9c, 0xf8, 0x6c, 0x84, 0x77, 21 | 0xff, 0x54, 0x78, 0x26, 0x5f, 0xbe, 0xe8, 0x1e, 22 | 0x36, 0x9f, 0x34, 0x80, 0x5c, 0x45, 0x2c, 0x9b, 23 | 0x76, 0xd5, 0x1b, 0x8f, 0xcc, 0xc3, 0xb8, 0xf5, 24 | } 25 | 26 | xorClientKey = []byte{ 27 | 0x78, 0x06, 0xad, 0x4c, 0x33, 0x86, 0x5d, 0x18, 28 | 0x4c, 0x01, 0x3f, 0x46, 29 | } 30 | ) 31 | 32 | func xorDeriveKey(seed []byte, size int) []byte { 33 | key := make([]byte, size) 34 | for i := 0; i < size; i++ { 35 | key[i] = (seed[i] + xorKeySeed[size*i]) & 0xff 36 | key[i] ^= xorKeySeed[size*(size-i-1)] 37 | } 38 | return key 39 | } 40 | 41 | func xorTransform(data []byte, key []byte) { 42 | dataSize, keySize := len(data), len(key) 43 | mod := dataSize % 4 44 | if mod > 0 { 45 | for i := 0; i < mod; i++ { 46 | data[i] ^= key[i%keySize] 47 | } 48 | } 49 | for i := mod; i < dataSize; i++ { 50 | data[i] ^= key[(i-mod)%keySize] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /internal/impl/api.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "io" 7 | "time" 8 | 9 | "github.com/deadblue/elevengo/internal/util" 10 | "github.com/deadblue/elevengo/lowlevel/client" 11 | ) 12 | 13 | func (c *ClientImpl) CallApi(spec client.ApiSpec, context context.Context) (err error) { 14 | payload := spec.Payload() 15 | if spec.IsCrypto() { 16 | spec.SetCryptoKey(c.ecc.EncodeToken(time.Now().UnixMilli())) 17 | payload, _ = c.encryptPayload(payload) 18 | } 19 | // Perform HTTP request 20 | var body io.ReadCloser 21 | if body, err = c.internalCall(spec.Url(), payload, context); err != nil { 22 | return 23 | } 24 | defer util.QuietlyClose(body) 25 | // Handle response 26 | if spec.IsCrypto() { 27 | var data []byte 28 | if data, err = c.decryptBody(body); err != nil { 29 | return 30 | } 31 | return spec.Parse(bytes.NewReader(data)) 32 | } else { 33 | return spec.Parse(body) 34 | } 35 | } 36 | 37 | func (c *ClientImpl) internalCall( 38 | url string, payload client.Payload, context context.Context, 39 | ) (body io.ReadCloser, err error) { 40 | c.v.Wait() 41 | defer c.v.ClockIn() 42 | // Prepare request 43 | if payload != nil { 44 | body, err = c.Post(url, payload, nil, context) 45 | } else { 46 | body, err = c.Get(url, nil, context) 47 | } 48 | return 49 | } 50 | 51 | func (c *ClientImpl) encryptPayload(p client.Payload) (ep client.Payload, err error) { 52 | if p == nil { 53 | return nil, nil 54 | } 55 | // Read payload 56 | body, err := io.ReadAll(p) 57 | if err != nil { 58 | return 59 | } 60 | // Encrypt it 61 | body = c.ecc.Encode(body) 62 | ep = CustomPayload(body, p.ContentType()) 63 | return 64 | } 65 | 66 | func (c *ClientImpl) decryptBody(r io.Reader) (data []byte, err error) { 67 | data, err = io.ReadAll(r) 68 | if err != nil { 69 | return 70 | } 71 | return c.ecc.Decode(data) 72 | } 73 | -------------------------------------------------------------------------------- /internal/impl/body.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import "io" 4 | 5 | type _BodyImpl struct { 6 | rc io.ReadCloser 7 | 8 | size int64 9 | } 10 | 11 | func (i *_BodyImpl) Read(p []byte) (int, error) { 12 | return i.rc.Read(p) 13 | } 14 | 15 | func (i *_BodyImpl) Close() error { 16 | return i.rc.Close() 17 | } 18 | 19 | func (i *_BodyImpl) Size() int64 { 20 | return i.size 21 | } 22 | -------------------------------------------------------------------------------- /internal/impl/client.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "net/http" 5 | "net/http/cookiejar" 6 | 7 | "github.com/deadblue/elevengo/internal/crypto/ec115" 8 | "github.com/deadblue/elevengo/plugin" 9 | ) 10 | 11 | type ClientImpl struct { 12 | // HTTP client 13 | hc plugin.HttpClient 14 | 15 | // Cookie jar 16 | cj http.CookieJar 17 | 18 | // Should Client manage cookie 19 | mc bool 20 | 21 | // User agent 22 | ua string 23 | 24 | // EC cipher 25 | ecc *ec115.Cipher 26 | 27 | // Valve to control API request frequency 28 | v Valve 29 | } 30 | 31 | func NewClient(hc plugin.HttpClient, cdMin uint, cdMax uint) *ClientImpl { 32 | impl := &ClientImpl{ 33 | ecc: ec115.New(), 34 | } 35 | if hc == nil { 36 | impl.cj, _ = cookiejar.New(nil) 37 | impl.hc = defaultHttpClient(impl.cj) 38 | impl.mc = false 39 | } else { 40 | impl.hc = hc 41 | switch hc := hc.(type) { 42 | case *http.Client: 43 | impl.cj = hc.Jar 44 | case plugin.HttpClientWithJar: 45 | impl.cj = hc.Jar() 46 | } 47 | if impl.cj != nil { 48 | impl.mc = false 49 | } else { 50 | impl.mc = true 51 | impl.cj, _ = cookiejar.New(nil) 52 | } 53 | } 54 | if cdMax > 0 && cdMax >= cdMin { 55 | impl.v.enabled = true 56 | impl.v.cdMin, impl.v.cdMax = cdMin, cdMax 57 | } 58 | return impl 59 | } 60 | 61 | func (c *ClientImpl) SetUserAgent(name string) { 62 | c.ua = name 63 | } 64 | 65 | func (c *ClientImpl) GetUserAgent() string { 66 | return c.ua 67 | } 68 | -------------------------------------------------------------------------------- /internal/impl/common.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "context" 5 | "net" 6 | "net/http" 7 | 8 | "github.com/deadblue/elevengo/internal/util" 9 | "github.com/deadblue/elevengo/lowlevel/client" 10 | ) 11 | 12 | func isTimeoutError(err error) bool { 13 | if err == nil { 14 | return false 15 | } 16 | ne, ok := err.(net.Error) 17 | return ok && ne.Timeout() 18 | } 19 | 20 | // |send| sends an HTTP request, returns HTTP response or an error. 21 | func (c *ClientImpl) send(req *http.Request) (resp *http.Response, err error) { 22 | req.Header.Set("Accept", "*/*") 23 | // Always override user-agent 24 | ua := c.ua 25 | if ua == "" { 26 | ua = defaultUserAgent 27 | } 28 | req.Header.Set(headerUserAgent, ua) 29 | if c.mc { 30 | // Add cookie 31 | for _, cookie := range c.cj.Cookies(req.URL) { 32 | req.AddCookie(cookie) 33 | } 34 | } 35 | // Send request with retry 36 | for { 37 | if resp, err = c.hc.Do(req); !isTimeoutError(err) { 38 | break 39 | } 40 | } 41 | if err == nil && c.mc { 42 | // Save cookie 43 | c.cj.SetCookies(req.URL, resp.Cookies()) 44 | } 45 | return 46 | } 47 | 48 | func (c *ClientImpl) Post( 49 | url string, payload client.Payload, headers map[string]string, context context.Context, 50 | ) (body client.Body, err error) { 51 | req, err := http.NewRequestWithContext(context, http.MethodPost, url, payload) 52 | if err != nil { 53 | return 54 | } 55 | if len(headers) > 0 { 56 | for name, value := range headers { 57 | req.Header.Add(name, value) 58 | } 59 | } 60 | req.Header.Set(headerContentType, payload.ContentType()) 61 | if size := payload.ContentLength(); size > 0 { 62 | req.ContentLength = size 63 | } 64 | var resp *http.Response 65 | if resp, err = c.send(req); err == nil { 66 | body = makeClientBody(resp) 67 | } 68 | return 69 | } 70 | 71 | func (c *ClientImpl) Get( 72 | url string, headers map[string]string, context context.Context, 73 | ) (body client.Body, err error) { 74 | req, err := http.NewRequestWithContext(context, http.MethodGet, url, nil) 75 | if err != nil { 76 | return 77 | } 78 | if len(headers) > 0 { 79 | for name, value := range headers { 80 | req.Header.Add(name, value) 81 | } 82 | } 83 | if resp, err := c.send(req); err == nil { 84 | body = makeClientBody(resp) 85 | } 86 | return 87 | } 88 | 89 | func makeClientBody(resp *http.Response) client.Body { 90 | body := &_BodyImpl{ 91 | rc: resp.Body, 92 | size: -1, 93 | } 94 | if hv := resp.Header.Get("Content-Length"); hv != "" { 95 | body.size = util.ParseInt64(hv, -1) 96 | } 97 | return body 98 | } 99 | -------------------------------------------------------------------------------- /internal/impl/cookie.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "net/http" 5 | neturl "net/url" 6 | ) 7 | 8 | func (c *ClientImpl) ImportCookies(cookies map[string]string, domains ...string) { 9 | for _, domain := range domains { 10 | c.internalImportCookies(cookies, domain, "/") 11 | } 12 | } 13 | 14 | func (c *ClientImpl) internalImportCookies(cookies map[string]string, domain string, path string) { 15 | // Make a dummy URL for saving cookie 16 | url := &neturl.URL{ 17 | Scheme: "https", 18 | Path: "/", 19 | } 20 | if domain[0] == '.' { 21 | url.Host = "www" + domain 22 | } else { 23 | url.Host = domain 24 | } 25 | // Prepare cookies 26 | cks := make([]*http.Cookie, 0, len(cookies)) 27 | for name, value := range cookies { 28 | cookie := &http.Cookie{ 29 | Name: name, 30 | Value: value, 31 | Domain: domain, 32 | Path: path, 33 | HttpOnly: true, 34 | } 35 | cks = append(cks, cookie) 36 | } 37 | // Save cookies 38 | c.cj.SetCookies(url, cks) 39 | } 40 | 41 | func (c *ClientImpl) ExportCookies(url string) map[string]string { 42 | u, _ := neturl.Parse(url) 43 | cookies := make(map[string]string) 44 | for _, ck := range c.cj.Cookies(u) { 45 | cookies[ck.Name] = ck.Value 46 | } 47 | return cookies 48 | } 49 | -------------------------------------------------------------------------------- /internal/impl/default.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | const ( 10 | defaultUserAgent = "Mozilla/5.0" 11 | ) 12 | 13 | func defaultHttpClient(jar http.CookieJar) *http.Client { 14 | // Make a copy of the default transport 15 | transport := http.DefaultTransport.(*http.Transport).Clone() 16 | // Change some settings 17 | transport.MaxIdleConnsPerHost = 10 18 | transport.MaxConnsPerHost = 0 19 | transport.MaxIdleConns = 100 20 | transport.IdleConnTimeout = 60 * time.Second 21 | // Setup timeout 22 | transport.DialContext = (&net.Dialer{ 23 | Timeout: 10 * time.Second, 24 | KeepAlive: 60 * time.Second, 25 | }).DialContext 26 | transport.TLSHandshakeTimeout = 10 * time.Second 27 | transport.ResponseHeaderTimeout = 30 * time.Second 28 | // Make http.Client 29 | return &http.Client{ 30 | Transport: transport, 31 | Jar: jar, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/impl/header.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | const ( 4 | headerUserAgent = "User-Agent" 5 | 6 | headerContentType = "Content-Type" 7 | ) 8 | -------------------------------------------------------------------------------- /internal/impl/payload.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/deadblue/elevengo/lowlevel/client" 7 | ) 8 | 9 | const ( 10 | _ContentTypeWwwForm = "application/x-www-form-urlencoded" 11 | ) 12 | 13 | // PayloadImpl is a standard `Payload` implementation. 14 | type _PayloadImpl struct { 15 | r *bytes.Reader 16 | t string 17 | } 18 | 19 | func (pi *_PayloadImpl) Read(p []byte) (int, error) { 20 | return pi.r.Read(p) 21 | } 22 | 23 | func (pi *_PayloadImpl) ContentType() string { 24 | return pi.t 25 | } 26 | 27 | func (pi *_PayloadImpl) ContentLength() int64 { 28 | return pi.r.Size() 29 | } 30 | 31 | // WwwFormPayload constructs a www URL-encoded form payload. 32 | func WwwFormPayload(s string) client.Payload { 33 | body := []byte(s) 34 | return &_PayloadImpl{ 35 | r: bytes.NewReader(body), 36 | t: _ContentTypeWwwForm, 37 | } 38 | } 39 | 40 | // CustomPayload constructs payload with given data and MIME type. 41 | func CustomPayload(data []byte, mimeType string) client.Payload { 42 | return &_PayloadImpl{ 43 | r: bytes.NewReader(data), 44 | t: mimeType, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /internal/impl/valve.go: -------------------------------------------------------------------------------- 1 | package impl 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | type Valve struct { 9 | // Is valve enabled? 10 | enabled bool 11 | // Last API calling finish time 12 | lastTime int64 13 | // Cooldown duration range 14 | cdMin, cdMax uint 15 | } 16 | 17 | func (v *Valve) ClockIn() { 18 | if !v.enabled { 19 | return 20 | } 21 | // Save last time 22 | v.lastTime = time.Now().UnixMilli() 23 | } 24 | 25 | func (v *Valve) Wait() { 26 | if !v.enabled { 27 | return 28 | } 29 | cd := v.getCooldownDuration() 30 | if cd == 0 { 31 | return 32 | } 33 | sleepDuration := v.lastTime + cd - time.Now().UnixMilli() 34 | if sleepDuration > 0 { 35 | time.Sleep(time.Duration(sleepDuration) * time.Millisecond) 36 | } 37 | } 38 | 39 | func (v *Valve) getCooldownDuration() int64 { 40 | // Skip invalid cooldown duration 41 | if v.cdMax == 0 || v.cdMax < v.cdMin { 42 | return 0 43 | } 44 | // Generate random duration in range 45 | var duration int64 46 | if v.cdMax == v.cdMin { 47 | duration = int64(v.cdMax) 48 | } else { 49 | duration = rand.Int63n(int64(v.cdMax-v.cdMin)) + int64(v.cdMin) 50 | } 51 | return duration 52 | } 53 | -------------------------------------------------------------------------------- /internal/multipart/boundary.go: -------------------------------------------------------------------------------- 1 | package multipart 2 | 3 | import ( 4 | "crypto/rand" 5 | "strings" 6 | ) 7 | 8 | var ( 9 | chars = []rune("1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz") 10 | charsCount = len(chars) 11 | ) 12 | 13 | func generateBoundary(prefix string, length int) string { 14 | seed := make([]byte, length) 15 | _, _ = rand.Read(seed) 16 | 17 | buf, n := strings.Builder{}, len(prefix) 18 | buf.Grow(length + n) 19 | if n > 0 { 20 | buf.WriteString(prefix) 21 | } 22 | for _, b := range seed { 23 | index := int(b) % charsCount 24 | buf.WriteRune(chars[index]) 25 | } 26 | return buf.String() 27 | } 28 | -------------------------------------------------------------------------------- /internal/multipart/builder.go: -------------------------------------------------------------------------------- 1 | package multipart 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/deadblue/elevengo/internal/util" 9 | ) 10 | 11 | type FormBuilder interface { 12 | AddValue(name, value string) FormBuilder 13 | AddFile(name, filename string, filesize int64, r io.Reader) FormBuilder 14 | Build() *Form 15 | } 16 | 17 | type implFormBuilder struct { 18 | // Boundary 19 | b string 20 | // Total size 21 | s int64 22 | // Readers 23 | rs []io.Reader 24 | // Reader number 25 | rn int 26 | } 27 | 28 | func (b *implFormBuilder) buffer() *bytes.Buffer { 29 | var buf *bytes.Buffer 30 | if b.rn == 0 { 31 | // Create bytes.Buffer and append it to rs 32 | buf = &bytes.Buffer{} 33 | b.rs = append(b.rs, buf) 34 | b.rn += 1 35 | } else { 36 | // The last reader is always bytes.Buffer! 37 | buf = (b.rs[b.rn-1]).(*bytes.Buffer) 38 | } 39 | return buf 40 | } 41 | 42 | func (b *implFormBuilder) incrSize(s int64) { 43 | if s < 0 { 44 | b.s = -1 45 | } else { 46 | if b.s >= 0 { 47 | b.s += s 48 | } 49 | } 50 | } 51 | 52 | func (b *implFormBuilder) AddValue(name string, value string) FormBuilder { 53 | // Calculate part size 54 | size := len(b.b) + len(name) + len(value) + 49 55 | // Write part 56 | buf := b.buffer() 57 | buf.Grow(size) 58 | buf.WriteString("--") 59 | buf.WriteString(b.b) 60 | buf.WriteString("\r\nContent-Disposition: form-data; name=\"") 61 | buf.WriteString(name) 62 | buf.WriteString("\"\r\n\r\n") 63 | buf.WriteString(value) 64 | buf.WriteString("\r\n") 65 | // Update size 66 | b.incrSize(int64(size)) 67 | return b 68 | } 69 | 70 | func (b *implFormBuilder) AddFile(name string, filename string, filesize int64, r io.Reader) FormBuilder { 71 | // Calculate part header size 72 | size := len(b.b) + len(name) + len(filename) + 60 73 | // Write part header 74 | buf := b.buffer() 75 | buf.Grow(size) 76 | buf.WriteString("--") 77 | buf.WriteString(b.b) 78 | buf.WriteString("\r\nContent-Disposition: form-data; name=\"") 79 | buf.WriteString(name) 80 | buf.WriteString("\"; filename=\"") 81 | buf.WriteString(filename) 82 | buf.WriteString("\"\r\n\r\n") 83 | b.incrSize(int64(size)) 84 | 85 | // Write part tail 86 | buf = &bytes.Buffer{} 87 | buf.WriteString("\r\n") 88 | 89 | b.rs = append(b.rs, r, buf) 90 | b.rn += 2 91 | 92 | // Update form size 93 | if filesize <= 0 { 94 | filesize = util.GuessSize(r) 95 | } 96 | b.incrSize(filesize + 2) 97 | 98 | return b 99 | } 100 | 101 | func (b *implFormBuilder) Build() *Form { 102 | if b.rn == 0 { 103 | return nil 104 | } 105 | // Write form tail 106 | size := len(b.b) + 4 107 | buf := b.buffer() 108 | buf.Grow(size) 109 | buf.WriteString("--") 110 | buf.WriteString(b.b) 111 | buf.WriteString("--") 112 | b.s += int64(size) 113 | // Build form 114 | return &Form{ 115 | t: fmt.Sprintf("multipart/form-data; boundary=%s", b.b), 116 | s: b.s, 117 | rs: b.rs, 118 | ri: 0, 119 | rn: b.rn, 120 | } 121 | } 122 | 123 | func Builder() FormBuilder { 124 | return &implFormBuilder{ 125 | b: generateBoundary("--ElevenGo--", 16), 126 | s: 0, 127 | rn: 0, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /internal/multipart/form.go: -------------------------------------------------------------------------------- 1 | package multipart 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | type Form struct { 8 | // Content type 9 | t string 10 | // Total Size 11 | s int64 12 | // Readers 13 | rs []io.Reader 14 | // Reader index & count 15 | ri, rn int 16 | } 17 | 18 | func (f *Form) ContentType() string { 19 | return f.t 20 | } 21 | 22 | func (f *Form) ContentLength() int64 { 23 | return f.s 24 | } 25 | 26 | func (f *Form) Read(p []byte) (n int, err error) { 27 | err = io.EOF 28 | for f.ri < f.rn && err == io.EOF { 29 | r, n0 := f.rs[f.ri], 0 30 | 31 | n0, err = r.Read(p) 32 | n += n0 33 | if err == io.EOF { 34 | f.ri += 1 35 | p = p[n0:] 36 | } 37 | } 38 | return 39 | } 40 | -------------------------------------------------------------------------------- /internal/oss/callback.go: -------------------------------------------------------------------------------- 1 | package oss 2 | 3 | import ( 4 | "encoding/json" 5 | "strings" 6 | ) 7 | 8 | type Callback struct { 9 | CallbackUrl string `json:"callbackUrl"` 10 | CallbackBody string `json:"callbackBody"` 11 | // Optional fields 12 | CallbackHost *string `json:"callbackHost,omitempty"` 13 | CallbackSNI *bool `json:"callbackSNI,omitempty"` 14 | CallbackBodyType *string `json:"callbackBodyType,omitempty"` 15 | } 16 | 17 | func ReplaceCallbackSha1(callback, fileSha1 string) string { 18 | cbObj := &Callback{} 19 | if err := json.Unmarshal([]byte(callback), cbObj); err != nil { 20 | return callback 21 | } 22 | cbObj.CallbackBody = strings.ReplaceAll(cbObj.CallbackBody, "${sha1}", fileSha1) 23 | cbData, _ := json.Marshal(cbObj) 24 | return string(cbData) 25 | } 26 | -------------------------------------------------------------------------------- /internal/oss/date.go: -------------------------------------------------------------------------------- 1 | package oss 2 | 3 | import "time" 4 | 5 | var GMT *time.Location 6 | 7 | func Date() string { 8 | now := time.Now().In(GMT) 9 | return now.Format(time.RFC1123) 10 | } 11 | 12 | func init() { 13 | GMT, _ = time.LoadLocation("GMT") 14 | } 15 | -------------------------------------------------------------------------------- /internal/oss/header.go: -------------------------------------------------------------------------------- 1 | package oss 2 | 3 | const ( 4 | HeaderAuthorization = "Authorization" 5 | HeaderDate = "Date" 6 | 7 | HeaderContentLength = "Content-Length" 8 | HeaderContentType = "Content-Type" 9 | HeaderContentMd5 = "Content-MD5" 10 | 11 | HeaderOssCallback = "X-OSS-Callback" 12 | HeaderOssCallbackVar = "X-OSS-Callback-Var" 13 | HeaderOssSecurityToken = "X-OSS-Security-Token" 14 | ) 15 | -------------------------------------------------------------------------------- /internal/oss/sign.go: -------------------------------------------------------------------------------- 1 | package oss 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha1" 6 | "encoding/base64" 7 | "fmt" 8 | "strings" 9 | 10 | "github.com/deadblue/elevengo/internal/util" 11 | ) 12 | 13 | type RequestMetadata struct { 14 | // Request verb 15 | Verb string 16 | // Request header 17 | Header map[string]string 18 | // OSS bucket name 19 | Bucket string 20 | // OSS object name 21 | Object string 22 | // OSS Parameters 23 | Params map[string]string 24 | } 25 | 26 | // CalculateAuthorization calculates authorization for OSS request 27 | func CalculateAuthorization(metadata *RequestMetadata, keyId string, keySecret string) string { 28 | // Create signer 29 | signer := hmac.New(sha1.New, []byte(keySecret)) 30 | wx := util.UpgradeWriter(signer) 31 | 32 | // Common parameters 33 | contentMd5 := metadata.Header[HeaderContentMd5] 34 | contentType := metadata.Header[HeaderContentType] 35 | date := metadata.Header[HeaderDate] 36 | wx.MustWriteString(metadata.Verb, "\n", contentMd5, "\n", contentType, "\n", date, "\n") 37 | 38 | // Canonicalized OSS Headers 39 | headers := make([]*Pair, 0, len(metadata.Header)) 40 | for name, value := range metadata.Header { 41 | name = strings.ToLower(name) 42 | if strings.HasPrefix(name, headerPrefixOss) { 43 | headers = append(headers, &Pair{ 44 | First: name, 45 | Last: value, 46 | }) 47 | } 48 | } 49 | sortPairs(headers) 50 | for _, header := range headers { 51 | wx.MustWriteString(header.First, ":", header.Last, "\n") 52 | } 53 | 54 | // Canonicalized Resource 55 | wx.MustWriteString("/", metadata.Bucket, "/", metadata.Object) 56 | // Sub resources 57 | if len(metadata.Params) > 0 { 58 | params := make([]*Pair, 0, len(metadata.Params)) 59 | for name, value := range metadata.Params { 60 | if _, ok := signingKeyMap[name]; ok { 61 | params = append(params, &Pair{ 62 | First: name, 63 | Last: value, 64 | }) 65 | } 66 | } 67 | sortPairs(params) 68 | for index, param := range params { 69 | if index == 0 { 70 | wx.MustWriteString("?", param.First) 71 | } else { 72 | wx.MustWriteString("&", param.First) 73 | } 74 | if param.Last != "" { 75 | wx.MustWriteString("=", param.Last) 76 | } 77 | } 78 | } 79 | 80 | signature := base64.StdEncoding.EncodeToString(signer.Sum(nil)) 81 | return fmt.Sprintf("OSS %s:%s", keyId, signature) 82 | } 83 | -------------------------------------------------------------------------------- /internal/oss/upload.go: -------------------------------------------------------------------------------- 1 | package oss 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | Region = "cn-shenzhen" 9 | 10 | endpointHost = "oss-cn-shenzhen.aliyuncs.com" 11 | ) 12 | 13 | func GetPutObjectUrl(bucket, key string) string { 14 | return fmt.Sprintf("https://%s.%s/%s", bucket, endpointHost, key) 15 | } 16 | 17 | func GetEndpointUrl() string { 18 | return fmt.Sprintf("https://%s", endpointHost) 19 | } 20 | -------------------------------------------------------------------------------- /internal/oss/util.go: -------------------------------------------------------------------------------- 1 | package oss 2 | 3 | import ( 4 | "bytes" 5 | "sort" 6 | ) 7 | 8 | const ( 9 | headerPrefixOss = "x-oss-" 10 | ) 11 | 12 | var ( 13 | signingKeyMap = map[string]bool{ 14 | "acl": true, 15 | "uploads": true, 16 | "location": true, 17 | "cors": true, 18 | "logging": true, 19 | "website": true, 20 | "referer": true, 21 | "lifecycle": true, 22 | "delete": true, 23 | "append": true, 24 | "tagging": true, 25 | "objectMeta": true, 26 | "uploadId": true, 27 | "partNumber": true, 28 | "security-token": true, 29 | "position": true, 30 | "img": true, 31 | "style": true, 32 | "styleName": true, 33 | "replication": true, 34 | "replicationProgress": true, 35 | "replicationLocation": true, 36 | "cname": true, 37 | "bucketInfo": true, 38 | "comp": true, 39 | "qos": true, 40 | "live": true, 41 | "status": true, 42 | "vod": true, 43 | "startTime": true, 44 | "endTime": true, 45 | "symlink": true, 46 | "x-oss-process": true, 47 | "response-content-type": true, 48 | "x-oss-traffic-limit": true, 49 | "response-content-language": true, 50 | "response-expires": true, 51 | "response-cache-control": true, 52 | "response-content-disposition": true, 53 | "response-content-encoding": true, 54 | "udf": true, 55 | "udfName": true, 56 | "udfImage": true, 57 | "udfId": true, 58 | "udfImageDesc": true, 59 | "udfApplication": true, 60 | "udfApplicationLog": true, 61 | "restore": true, 62 | "callback": true, 63 | "callback-var": true, 64 | "qosInfo": true, 65 | "policy": true, 66 | "stat": true, 67 | "encryption": true, 68 | "versions": true, 69 | "versioning": true, 70 | "versionId": true, 71 | "requestPayment": true, 72 | "x-oss-request-payer": true, 73 | "sequential": true, 74 | "inventory": true, 75 | "inventoryId": true, 76 | "continuation-token": true, 77 | "asyncFetch": true, 78 | "worm": true, 79 | "wormId": true, 80 | "wormExtend": true, 81 | "withHashContext": true, 82 | "x-oss-enable-md5": true, 83 | "x-oss-enable-sha1": true, 84 | "x-oss-enable-sha256": true, 85 | "x-oss-hash-ctx": true, 86 | "x-oss-md5-ctx": true, 87 | "transferAcceleration": true, 88 | "regionList": true, 89 | } 90 | ) 91 | 92 | type Pair struct { 93 | First string 94 | Last string 95 | } 96 | 97 | type Pairs []*Pair 98 | 99 | func (h Pairs) Len() int { 100 | return len(h) 101 | } 102 | 103 | func (h Pairs) Less(i, j int) bool { 104 | return bytes.Compare([]byte(h[i].First), []byte(h[j].First)) < 0 105 | } 106 | 107 | func (h Pairs) Swap(i, j int) { 108 | h[i], h[j] = h[j], h[i] 109 | } 110 | 111 | func sortPairs(pairs []*Pair) { 112 | sort.Sort(Pairs(pairs)) 113 | } 114 | -------------------------------------------------------------------------------- /internal/protocol/basic.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | "github.com/deadblue/elevengo/lowlevel/errors" 8 | ) 9 | 10 | // _BasicResp is the basic response for most JSON/JSONP API. 11 | type BasicResp struct { 12 | // Response state 13 | State bool `json:"state"` 14 | // Possible error code fields 15 | ErrorCode util.IntNumber `json:"errno,omitempty"` 16 | ErrorCode2 int `json:"errNo,omitempty"` 17 | ErrorCode3 int `json:"errcode,omitempty"` 18 | ErrorCode4 int `json:"errCode,omitempty"` 19 | ErrorCode5 int `json:"code,omitempty"` 20 | // Possible error message fields 21 | ErrorMessage string `json:"error,omitempty"` 22 | ErrorMessage2 string `json:"message,omitempty"` 23 | ErrorMessage3 string `json:"error_msg,omitempty"` 24 | } 25 | 26 | func (r *BasicResp) Err() error { 27 | if r.State { 28 | return nil 29 | } 30 | return errors.Get(util.NonZero( 31 | r.ErrorCode.Int(), 32 | r.ErrorCode2, 33 | r.ErrorCode3, 34 | r.ErrorCode4, 35 | r.ErrorCode5, 36 | ), util.NonEmptyString( 37 | r.ErrorMessage, 38 | r.ErrorMessage2, 39 | r.ErrorMessage3, 40 | )) 41 | } 42 | 43 | // StandardResp is the response for all JSON/JSONP APIs with "data" field. 44 | type StandardResp struct { 45 | BasicResp 46 | 47 | Data json.RawMessage `json:"data"` 48 | } 49 | 50 | func (r *StandardResp) Extract(v any) error { 51 | return json.Unmarshal(r.Data, v) 52 | } 53 | -------------------------------------------------------------------------------- /internal/protocol/cookie.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "strings" 4 | 5 | const ( 6 | CookieUrl = "https://115.com" 7 | 8 | CookieNameUID = "UID" 9 | CookieNameCID = "CID" 10 | CookieNameKID = "KID" 11 | CookieNameSEID = "SEID" 12 | ) 13 | 14 | var ( 15 | CookieDomains = []string{ 16 | ".115.com", 17 | ".anxia.com", 18 | } 19 | ) 20 | 21 | func IsWebCredential(uid string) bool { 22 | parts := strings.Split(uid, "_") 23 | return len(parts) == 3 && parts[1][0] == 'A' 24 | } 25 | -------------------------------------------------------------------------------- /internal/protocol/dir.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "github.com/deadblue/elevengo/internal/util" 4 | 5 | //lint:ignore U1000 This type is used in generic. 6 | type DirCreateResp struct { 7 | BasicResp 8 | 9 | FileId string `json:"file_id"` 10 | FileName string `json:"file_name"` 11 | } 12 | 13 | func (r *DirCreateResp) Extract(v *string) (err error) { 14 | *v = r.FileId 15 | return 16 | } 17 | 18 | //lint:ignore U1000 This type is used in generic. 19 | type DirLocateResp struct { 20 | BasicResp 21 | 22 | DirId util.IntNumber `json:"id"` 23 | IsPrivate util.IntNumber `json:"is_private"` 24 | } 25 | 26 | func (r *DirLocateResp) Extract(v *string) (err error) { 27 | *v = r.DirId.String() 28 | return 29 | } 30 | -------------------------------------------------------------------------------- /internal/protocol/file.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | "github.com/deadblue/elevengo/lowlevel/errors" 8 | "github.com/deadblue/elevengo/lowlevel/types" 9 | ) 10 | 11 | const ( 12 | FileListLimit = 32 13 | ) 14 | 15 | //lint:ignore U1000 This type is used in generic. 16 | type FileListResp struct { 17 | StandardResp 18 | 19 | AreaId string `json:"aid"` 20 | CategoryId util.IntNumber `json:"cid"` 21 | 22 | Count int `json:"count"` 23 | 24 | Order string `json:"order"` 25 | IsAsc int `json:"is_asc"` 26 | 27 | Offset int `json:"offset"` 28 | Limit int `json:"limit"` 29 | } 30 | 31 | func (r *FileListResp) Err() (err error) { 32 | // Handle special error 33 | if r.ErrorCode2 == errors.CodeFileOrderNotSupported { 34 | return &errors.FileOrderInvalidError{ 35 | Order: r.Order, 36 | Asc: r.IsAsc, 37 | } 38 | } 39 | return r.StandardResp.Err() 40 | } 41 | 42 | func (r *FileListResp) Extract(v *types.FileListResult) (err error) { 43 | v.Files = make([]*types.FileInfo, 0) 44 | if err = json.Unmarshal(r.Data, &v.Files); err != nil { 45 | return 46 | } 47 | v.DirId = r.CategoryId.String() 48 | v.Count = r.Count 49 | v.Order, v.Asc = r.Order, r.IsAsc 50 | return 51 | } 52 | 53 | //lint:ignore U1000 This type is used in generic. 54 | type FileSearchResp struct { 55 | StandardResp 56 | 57 | Folder struct { 58 | CategoryId string `json:"cid"` 59 | ParentId string `json:"pid"` 60 | Name string `json:"name"` 61 | } `json:"folder"` 62 | 63 | Count int `json:"count"` 64 | FileCount int `json:"file_count"` 65 | FolderCount int `json:"folder_count"` 66 | 67 | Order string `json:"order"` 68 | IsAsc int `json:"is_asc"` 69 | 70 | Offset int `json:"offset"` 71 | Limit int `json:"page_size"` 72 | } 73 | 74 | func (r *FileSearchResp) Extract(v *types.FileListResult) (err error) { 75 | v.Files = make([]*types.FileInfo, 0) 76 | if err = json.Unmarshal(r.Data, &v.Files); err != nil { 77 | return 78 | } 79 | v.DirId = r.Folder.CategoryId 80 | v.Count = r.Count 81 | v.Order, v.Asc = r.Order, r.IsAsc 82 | return 83 | } 84 | 85 | //lint:ignore U1000 This type is used in generic. 86 | type FileGetDescResp struct { 87 | BasicResp 88 | 89 | Desc string `json:"desc"` 90 | } 91 | 92 | func (r *FileGetDescResp) Extract(v *string) (err error) { 93 | *v = r.Desc 94 | return 95 | } 96 | -------------------------------------------------------------------------------- /internal/protocol/label.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | const ( 4 | LabelListLimit = 11500 5 | ) 6 | -------------------------------------------------------------------------------- /internal/protocol/name.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import "fmt" 4 | 5 | const ( 6 | namePrefix = "Mozilla/5.0" 7 | ) 8 | 9 | func MakeUserAgent(name, appName, appVer string) string { 10 | if name == "" { 11 | return fmt.Sprintf("%s %s/%s", namePrefix, appName, appVer) 12 | } else { 13 | return fmt.Sprintf("%s %s %s/%s", namePrefix, name, appName, appVer) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/protocol/offline.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/lowlevel/types" 5 | ) 6 | 7 | //lint:ignore U1000 This type is used in generic. 8 | type OfflineListResp struct { 9 | BasicResp 10 | 11 | PageIndex int `json:"page"` 12 | PageCount int `json:"page_count"` 13 | PageSize int `json:"page_row"` 14 | 15 | QuotaTotal int `json:"total"` 16 | QuotaRemain int `json:"quota"` 17 | 18 | TaskCount int `json:"count"` 19 | Tasks []*types.TaskInfo `json:"tasks"` 20 | } 21 | 22 | func (r *OfflineListResp) Extract(v *types.OfflineListResult) (err error) { 23 | v.PageIndex = r.PageIndex 24 | v.PageCount = r.PageCount 25 | v.PageSize = r.PageSize 26 | v.QuotaTotal = r.QuotaTotal 27 | v.QuotaRemain = r.QuotaRemain 28 | v.TaskCount = r.TaskCount 29 | v.Tasks = append(v.Tasks, r.Tasks...) 30 | return nil 31 | } 32 | -------------------------------------------------------------------------------- /internal/protocol/qrcode.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | "github.com/deadblue/elevengo/lowlevel/errors" 8 | ) 9 | 10 | //lint:ignore U1000 This type is used in generic. 11 | type QrcodeBaseResp struct { 12 | State int `json:"state"` 13 | ErrorCode1 int `json:"code"` 14 | ErrorCode2 int `json:"errno"` 15 | ErrorMessage1 string `json:"message"` 16 | ErrorMessage2 string `json:"error"` 17 | 18 | Data json.RawMessage `json:"data"` 19 | } 20 | 21 | func (r *QrcodeBaseResp) Err() error { 22 | if r.State != 0 { 23 | return nil 24 | } 25 | return errors.Get(r.ErrorCode1, util.NonEmptyString( 26 | r.ErrorMessage1, r.ErrorMessage2, 27 | )) 28 | } 29 | 30 | func (r *QrcodeBaseResp) Extract(v any) error { 31 | return json.Unmarshal(r.Data, v) 32 | } 33 | -------------------------------------------------------------------------------- /internal/protocol/recycle.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/internal/util" 5 | "github.com/deadblue/elevengo/lowlevel/types" 6 | ) 7 | 8 | //lint:ignore U1000 This type is used in generic. 9 | type RecycleBinListResp struct { 10 | StandardResp 11 | 12 | Count util.IntNumber `json:"count"` 13 | 14 | Offset int `json:"offset"` 15 | Limit int `json:"page_size"` 16 | } 17 | 18 | func (r *RecycleBinListResp) Extract(v *types.RecycleBinListResult) (err error) { 19 | if err = r.StandardResp.Extract(&v.Item); err != nil { 20 | return 21 | } 22 | v.Count = r.Count.Int() 23 | return 24 | } 25 | -------------------------------------------------------------------------------- /internal/protocol/share.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | "github.com/deadblue/elevengo/lowlevel/types" 8 | ) 9 | 10 | //lint:ignore U1000 This type is used in generic. 11 | type ShareListResp struct { 12 | BasicResp 13 | 14 | Count int `json:"count"` 15 | List json.RawMessage `json:"list"` 16 | } 17 | 18 | func (r *ShareListResp) Extract(v *types.ShareListResult) (err error) { 19 | if err = json.Unmarshal(r.List, &v.Items); err != nil { 20 | return 21 | } 22 | v.Count = r.Count 23 | return 24 | } 25 | 26 | type ShareFileProto struct { 27 | FileId string `json:"fid"` 28 | DirId util.IntNumber `json:"cid"` 29 | ParentId string `json:"pid"` 30 | Name string `json:"n"` 31 | IsFile int `json:"fc"` 32 | Size int64 `json:"s"` 33 | Sha1 string `json:"sha"` 34 | CreateTime util.IntNumber `json:"t"` 35 | 36 | IsVideo int `json:"iv"` 37 | VideoDefinition int `json:"vdi"` 38 | AudioPlayLong int `json:"audio_play_long"` 39 | VideoPlayLong int `json:"play_long"` 40 | } 41 | 42 | type ShareSnapProto struct { 43 | UserInfo struct { 44 | UserId util.IntNumber `json:"user_id"` 45 | UserName string `json:"user_name"` 46 | AvatarUrl string `json:"face"` 47 | } `json:"userinfo"` 48 | UserAppeal struct { 49 | CanAppeal int `json:"can_appeal"` 50 | CanShareAppeal int `json:"can_share_appeal"` 51 | CanGlobalAppeal int `json:"can_global_appeal"` 52 | PopupAppealPage int `json:"popup_appeal_page"` 53 | } `json:"user_appeal"` 54 | ShareInfo struct { 55 | SnapId string `json:"snap_id"` 56 | ShareTitle string `json:"share_title"` 57 | ShareState util.IntNumber `json:"share_state"` 58 | ShareSize util.IntNumber `json:"file_size"` 59 | ReceiveCode string `json:"receive_code"` 60 | ReceiveCount util.IntNumber `json:"receive_count"` 61 | CreateTime util.IntNumber `json:"create_time"` 62 | ExpireTime util.IntNumber `json:"expire_time"` 63 | AutoRenewal string `json:"auto_renewal"` 64 | AutoFillReceiveCode string `json:"auto_fill_recvcode"` 65 | CanReport int `json:"can_report"` 66 | CanNotice int `json:"can_notice"` 67 | HaveVioFile int `json:"have_vio_file"` 68 | } `json:"shareinfo"` 69 | ShareState util.IntNumber `json:"share_state"` 70 | 71 | Count int `json:"count"` 72 | Files []*ShareFileProto `json:"list"` 73 | } 74 | 75 | type ShareSnapResp struct { 76 | BasicResp 77 | 78 | Data ShareSnapProto `json:"data"` 79 | } 80 | 81 | func (r *ShareSnapResp) Extract(v *types.ShareSnapResult) (err error) { 82 | data := r.Data 83 | v.SnapId = data.ShareInfo.SnapId 84 | v.UserId = data.UserInfo.UserId.Int() 85 | v.ShareTitle = data.ShareInfo.ShareTitle 86 | v.ShareState = data.ShareInfo.ShareState.Int() 87 | v.ReceiveCount = data.ShareInfo.ReceiveCount.Int() 88 | v.CreateTime = data.ShareInfo.CreateTime.Int64() 89 | v.ExpireTime = data.ShareInfo.ExpireTime.Int64() 90 | 91 | v.TotalSize = data.ShareInfo.ShareSize.Int64() 92 | v.FileCount = data.Count 93 | v.Files = make([]*types.ShareFileInfo, len(data.Files)) 94 | for i, f := range data.Files { 95 | fileId := "" 96 | if f.IsFile == 1 { 97 | fileId = f.FileId 98 | } else { 99 | fileId = f.DirId.String() 100 | } 101 | v.Files[i] = &types.ShareFileInfo{ 102 | FileId: fileId, 103 | IsDir: f.IsFile == 0, 104 | Name: f.Name, 105 | Size: f.Size, 106 | Sha1: f.Sha1, 107 | CreateTime: f.CreateTime.Int64(), 108 | 109 | IsVideo: f.IsVideo != 0, 110 | VideoDefinition: f.VideoDefinition, 111 | MediaDuration: max(f.AudioPlayLong, f.VideoPlayLong), 112 | } 113 | } 114 | return 115 | } 116 | -------------------------------------------------------------------------------- /internal/protocol/upload.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/deadblue/elevengo/lowlevel/errors" 7 | "github.com/deadblue/elevengo/lowlevel/types" 8 | ) 9 | 10 | //lint:ignore U1000 This type is used in generic. 11 | type UploadInfoResp struct { 12 | BasicResp 13 | 14 | UserId int `json:"user_id"` 15 | UserKey string `json:"userkey"` 16 | } 17 | 18 | func (r *UploadInfoResp) Extract(v *types.UploadInfoResult) error { 19 | v.UserId = r.UserId 20 | v.UserKey = r.UserKey 21 | return nil 22 | } 23 | 24 | //lint:ignore U1000 This type is used in generic. 25 | type UploadInitResp struct { 26 | Request string `json:"request"` 27 | Version string `json:"version"` 28 | ErrorCode int `json:"statuscode"` 29 | ErrorMsg string `json:"statusmsg"` 30 | 31 | Status int `json:"status"` 32 | PickCode string `json:"pickcode"` 33 | 34 | // New fields in upload v4.0 35 | SignKey string `json:"sign_key"` 36 | SignCheck string `json:"sign_check"` 37 | 38 | // OSS upload fields 39 | Bucket string `json:"bucket"` 40 | Object string `json:"object"` 41 | Callback struct { 42 | Callback string `json:"callback"` 43 | CallbackVar string `json:"callback_var"` 44 | } `json:"callback"` 45 | 46 | // Useless fields 47 | FileId int `json:"fileid"` 48 | FileInfo string `json:"fileinfo"` 49 | Target string `json:"target"` 50 | } 51 | 52 | func (r *UploadInitResp) Err() error { 53 | // Ignore 701 error 54 | if r.ErrorCode == 0 || r.ErrorCode == 701 { 55 | return nil 56 | } 57 | return errors.ErrUnexpected 58 | } 59 | 60 | func (r *UploadInitResp) Extract(v *types.UploadInitResult) (err error) { 61 | switch r.Status { 62 | case 1: 63 | v.Oss.Bucket = r.Bucket 64 | v.Oss.Object = r.Object 65 | v.Oss.Callback = r.Callback.Callback 66 | v.Oss.CallbackVar = r.Callback.CallbackVar 67 | case 2: 68 | v.Exists = true 69 | v.PickCode = r.PickCode 70 | case 7: 71 | v.SignKey = r.SignKey 72 | v.SignCheck = r.SignCheck 73 | } 74 | return 75 | } 76 | 77 | //lint:ignore U1000 This type is used in generic. 78 | type UploadTokenResp struct { 79 | StatusCode string `json:"StatusCode"` 80 | AccessKeyId string `json:"AccessKeyId"` 81 | AccessKeySecret string `json:"AccessKeySecret"` 82 | SecurityToken string `json:"SecurityToken"` 83 | Expiration string `json:"Expiration"` 84 | } 85 | 86 | func (r *UploadTokenResp) Err() error { 87 | if r.StatusCode == "200" { 88 | return nil 89 | } 90 | return errors.ErrUnexpected 91 | } 92 | 93 | func (r *UploadTokenResp) Extract(v *types.UploadTokenResult) error { 94 | v.AccessKeyId = r.AccessKeyId 95 | v.AccessKeySecret = r.AccessKeySecret 96 | v.SecurityToken = r.SecurityToken 97 | v.Expiration, _ = time.Parse(time.RFC3339, r.Expiration) 98 | return nil 99 | } 100 | 101 | //lint:ignore U1000 This type is used in generic. 102 | type UploadSampleInitResp struct { 103 | Host string `json:"host"` 104 | Object string `json:"object"` 105 | Callback string `json:"callback"` 106 | AccessKeyId string `json:"accessid"` 107 | Policy string `json:"policy"` 108 | Signature string `json:"signature"` 109 | Expire int64 `json:"expire"` 110 | } 111 | 112 | func (r *UploadSampleInitResp) Err() error { 113 | return nil 114 | } 115 | 116 | func (r *UploadSampleInitResp) Extract(v *types.UploadSampleInitResult) error { 117 | v.Host = r.Host 118 | v.Object = r.Object 119 | v.Callback = r.Callback 120 | v.AccessKeyId = r.AccessKeyId 121 | v.Policy = r.Policy 122 | v.Signature = r.Signature 123 | return nil 124 | } 125 | -------------------------------------------------------------------------------- /internal/protocol/video.go: -------------------------------------------------------------------------------- 1 | package protocol 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/internal/util" 5 | "github.com/deadblue/elevengo/lowlevel/types" 6 | ) 7 | 8 | //lint:ignore U1000 This type is used in generic. 9 | type VideoPlayWebResp struct { 10 | BasicResp 11 | 12 | FileId string `json:"file_id"` 13 | ParentId string `json:"parent_id"` 14 | FileName string `json:"file_name"` 15 | FileSize util.IntNumber `json:"file_size"` 16 | FileSha1 string `json:"sha1"` 17 | PickCode string `json:"pick_code"` 18 | FileStatus int `json:"file_status"` 19 | VideoDuration util.FloatNumner `json:"play_long"` 20 | VideoWidth util.IntNumber `json:"width"` 21 | VideoHeight util.IntNumber `json:"height"` 22 | VideoUrl string `json:"video_url"` 23 | } 24 | 25 | func (r *VideoPlayWebResp) Extract(v *types.VideoPlayResult) error { 26 | v.IsReady = r.FileStatus == 1 27 | v.FileId = r.FileId 28 | v.FileName = r.FileName 29 | v.FileSize = r.FileSize.Int64() 30 | v.VideoDuration = r.VideoDuration.Float64() 31 | v.Videos = []*types.VideoInfo{ 32 | { 33 | Width: r.VideoWidth.Int(), 34 | Height: r.VideoHeight.Int(), 35 | PlayUrl: r.VideoUrl, 36 | }, 37 | } 38 | return nil 39 | } 40 | -------------------------------------------------------------------------------- /internal/upload/signature.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "crypto/sha1" 5 | 6 | "github.com/deadblue/elevengo/internal/crypto/hash" 7 | "github.com/deadblue/elevengo/internal/util" 8 | ) 9 | 10 | func CalcSignature(userId, userKey, fileId, target string) string { 11 | digester := sha1.New() 12 | wx := util.UpgradeWriter(digester) 13 | // First pass 14 | wx.MustWriteString(userId, fileId, target, "0") 15 | result := hash.ToHex(digester) 16 | // Second pass 17 | digester.Reset() 18 | wx.MustWriteString(userKey, result, "000000") 19 | return hash.ToHexUpper(digester) 20 | } 21 | -------------------------------------------------------------------------------- /internal/upload/token.go: -------------------------------------------------------------------------------- 1 | package upload 2 | 3 | import ( 4 | "crypto/md5" 5 | "strconv" 6 | 7 | "github.com/deadblue/elevengo/internal/crypto/hash" 8 | "github.com/deadblue/elevengo/internal/util" 9 | ) 10 | 11 | const ( 12 | tokenSalt = "Qclm8MGWUv59TnrR0XPg" 13 | ) 14 | 15 | func CalcToken( 16 | appVer, userId, userHash string, 17 | fileId string, fileSize int64, 18 | signKey, signValue string, 19 | timestamp int64, 20 | ) string { 21 | digester := md5.New() 22 | wx := util.UpgradeWriter(digester) 23 | wx.MustWriteString( 24 | tokenSalt, 25 | fileId, 26 | strconv.FormatInt(fileSize, 10), 27 | signKey, 28 | signValue, 29 | userId, 30 | strconv.FormatInt(timestamp, 10), 31 | userHash, 32 | appVer, 33 | ) 34 | return hash.ToHex(digester) 35 | } 36 | -------------------------------------------------------------------------------- /internal/util/base64.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "encoding/base64" 4 | 5 | func Base64Encode(src string) string { 6 | return base64.StdEncoding.EncodeToString([]byte(src)) 7 | } 8 | -------------------------------------------------------------------------------- /internal/util/cookie.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | func MarshalCookies(cookies map[string]string) string { 6 | if len(cookies) > 0 { 7 | buf, isFirst := strings.Builder{}, true 8 | for ck, cv := range cookies { 9 | if !isFirst { 10 | buf.WriteString("; ") 11 | } 12 | buf.WriteString(ck) 13 | buf.WriteRune('=') 14 | buf.WriteString(cv) 15 | isFirst = false 16 | } 17 | return buf.String() 18 | } 19 | return "" 20 | } 21 | -------------------------------------------------------------------------------- /internal/util/io.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | type WriterEx struct { 11 | w io.Writer 12 | } 13 | 14 | func (w *WriterEx) Write(p []byte) (n int, err error) { 15 | return w.w.Write(p) 16 | } 17 | 18 | func (w *WriterEx) WriteString(s string) (n int, err error) { 19 | return w.Write([]byte(s)) 20 | } 21 | 22 | func (w *WriterEx) WriteByte(b byte) (err error) { 23 | _, err = w.Write([]byte{b}) 24 | return 25 | } 26 | 27 | func (w *WriterEx) MustWriteString(s ...string) { 28 | for _, item := range s { 29 | if _, err := w.WriteString(item); err != nil { 30 | break 31 | } 32 | } 33 | } 34 | 35 | // UpgradeWriter gives you a powerful Writer than the original one! 36 | func UpgradeWriter(w io.Writer) *WriterEx { 37 | return &WriterEx{w: w} 38 | } 39 | 40 | func ConsumeReader(r io.ReadCloser) { 41 | _, _ = io.Copy(io.Discard, r) 42 | _ = r.Close() 43 | } 44 | 45 | func QuietlyClose(c io.Closer) { 46 | _ = c.Close() 47 | } 48 | 49 | func GuessSize(r io.Reader) (size int64) { 50 | size = -1 51 | switch r := r.(type) { 52 | case *bytes.Buffer: 53 | size = int64(r.Len()) 54 | case *bytes.Reader: 55 | size = r.Size() 56 | case *strings.Reader: 57 | size = int64(r.Len()) 58 | case *os.File: 59 | if i, e := r.Stat(); e == nil { 60 | size = i.Size() 61 | } 62 | } 63 | return 64 | } 65 | -------------------------------------------------------------------------------- /internal/util/json.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "encoding/json" 5 | "strconv" 6 | ) 7 | 8 | // IntNumber uses for JSON field which maybe a string or an integer number. 9 | type IntNumber int64 10 | 11 | func (n *IntNumber) UnmarshalJSON(b []byte) (err error) { 12 | var i int64 13 | if b[0] == '"' { 14 | var s string 15 | if err = json.Unmarshal(b, &s); err == nil { 16 | i, _ = strconv.ParseInt(s, 10, 64) 17 | } 18 | } else { 19 | err = json.Unmarshal(b, &i) 20 | } 21 | if err == nil { 22 | *n = IntNumber(i) 23 | } 24 | return 25 | } 26 | 27 | func (n IntNumber) Int64() int64 { 28 | return int64(n) 29 | } 30 | 31 | func (n IntNumber) Int() int { 32 | return int(n) 33 | } 34 | 35 | func (n IntNumber) String() string { 36 | return strconv.FormatInt(int64(n), 10) 37 | } 38 | 39 | // FloatNumner uses for JSON field which maybe a string or an float number. 40 | type FloatNumner float64 41 | 42 | func (n *FloatNumner) UnmarshalJSON(b []byte) (err error) { 43 | var f float64 44 | if b[0] == '"' { 45 | var s string 46 | if err = json.Unmarshal(b, &s); err == nil { 47 | f, err = strconv.ParseFloat(s, 64) 48 | } 49 | } else { 50 | err = json.Unmarshal(b, &f) 51 | } 52 | if err == nil { 53 | *n = FloatNumner(f) 54 | } 55 | return 56 | } 57 | 58 | func (n FloatNumner) Float64() float64 { 59 | return float64(n) 60 | } 61 | 62 | type Boolean bool 63 | 64 | func (b *Boolean) UnmarshalJSON(data []byte) (err error) { 65 | var v bool 66 | switch data[0] { 67 | case 'f': 68 | v = false 69 | case 't': 70 | v = true 71 | case '"': 72 | var s string 73 | if err = json.Unmarshal(data, &s); err == nil { 74 | v = s != "" 75 | } 76 | case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9': 77 | var i int 78 | if err = json.Unmarshal(data, &i); err == nil { 79 | v = i != 0 80 | } 81 | } 82 | *b = Boolean(v) 83 | return 84 | } 85 | -------------------------------------------------------------------------------- /internal/util/mime.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "mime" 5 | "path" 6 | ) 7 | 8 | const ( 9 | DefaultMimeType = "application/octet-stream" 10 | ) 11 | 12 | func DetermineMimeType(name string) string { 13 | if ext := path.Ext(name); ext != "" { 14 | if mt := mime.TypeByExtension(ext); mt != "" { 15 | return mt 16 | } 17 | } 18 | return DefaultMimeType 19 | } 20 | -------------------------------------------------------------------------------- /internal/util/params.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "net/url" 5 | "strconv" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | type Params map[string]string 11 | 12 | func (p Params) Set(key, value string) Params { 13 | p[key] = value 14 | return p 15 | } 16 | 17 | func (p Params) SetInt(key string, value int) Params { 18 | p.Set(key, strconv.Itoa(value)) 19 | return p 20 | } 21 | 22 | func (p Params) SetInt64(key string, value int64) Params { 23 | p.Set(key, strconv.FormatInt(value, 10)) 24 | return p 25 | } 26 | 27 | func (p Params) SetNow(key string) Params { 28 | now := time.Now().Unix() 29 | p.Set(key, strconv.FormatInt(now, 10)) 30 | return p 31 | } 32 | 33 | func (p Params) Encode() string { 34 | sb := &strings.Builder{} 35 | isFirst := true 36 | for key, value := range p { 37 | if !isFirst { 38 | sb.WriteRune('&') 39 | } 40 | sb.WriteString(url.QueryEscape(key)) 41 | sb.WriteRune('=') 42 | sb.WriteString(url.QueryEscape(value)) 43 | isFirst = false 44 | } 45 | return sb.String() 46 | } 47 | -------------------------------------------------------------------------------- /internal/util/time.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | ) 7 | 8 | const ( 9 | timeLayout = "2006-01-02 15:04" 10 | ) 11 | 12 | var ( 13 | timeLocation = time.FixedZone("CST", 8*60*60) 14 | ) 15 | 16 | func ParseFileTime(str string) time.Time { 17 | if isTimestamp(str) { 18 | sec, _ := strconv.ParseInt(str, 10, 64) 19 | return time.Unix(sec, 0) 20 | } else { 21 | t, _ := time.ParseInLocation(timeLayout, str, timeLocation) 22 | return t.UTC() 23 | } 24 | } 25 | 26 | func isTimestamp(str string) bool { 27 | for _, ch := range str { 28 | if ch < '0' || ch > '9' { 29 | return false 30 | } 31 | } 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /internal/util/url.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strings" 4 | 5 | func SecretUrl(url string) string { 6 | if index := strings.IndexRune(url, ':'); index > 0 { 7 | scheme := strings.ToLower(url[:index]) 8 | if scheme == "http" { 9 | return "https" + url[index:] 10 | } 11 | } 12 | return url 13 | } 14 | -------------------------------------------------------------------------------- /internal/util/value.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import "strconv" 4 | 5 | func NonZero(number ...int) int { 6 | for _, n := range number { 7 | if n != 0 { 8 | return n 9 | } 10 | } 11 | return 0 12 | } 13 | 14 | func NonEmptyString(str ...string) string { 15 | for _, s := range str { 16 | if s != "" { 17 | return s 18 | } 19 | } 20 | return "" 21 | } 22 | 23 | func NotNull[V any](values ...*V) *V { 24 | for _, value := range values { 25 | if value != nil { 26 | return value 27 | } 28 | } 29 | return nil 30 | } 31 | 32 | func ParseInt64(s string, defVal int64) int64 { 33 | if n, err := strconv.ParseInt(s, 10, 64); err == nil { 34 | return n 35 | } else { 36 | return defVal 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /iterator.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "errors" 5 | "iter" 6 | ) 7 | 8 | var ( 9 | errNoMoreItems = errors.New("no more items") 10 | ) 11 | 12 | // Iterator iterate items. 13 | type Iterator[T any] interface { 14 | 15 | // Count return the count of items. 16 | Count() int 17 | 18 | // Items return an index-item sequence. 19 | Items() iter.Seq2[int, *T] 20 | } 21 | -------------------------------------------------------------------------------- /label.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | 7 | "github.com/deadblue/elevengo/internal/protocol" 8 | "github.com/deadblue/elevengo/lowlevel/api" 9 | "github.com/deadblue/elevengo/lowlevel/client" 10 | "github.com/deadblue/elevengo/lowlevel/errors" 11 | "github.com/deadblue/elevengo/lowlevel/types" 12 | ) 13 | 14 | type LabelColor int 15 | 16 | const ( 17 | LabelColorBlank LabelColor = iota 18 | LabelColorRed 19 | LabelColorOrange 20 | LabelColorYellow 21 | LabelColorGreen 22 | LabelColorBlue 23 | LabelColorPurple 24 | LabelColorGray 25 | ) 26 | 27 | var ( 28 | labelColorMap = map[LabelColor]string{ 29 | LabelColorBlank: api.LabelColorBlank, 30 | LabelColorRed: api.LabelColorRed, 31 | LabelColorOrange: api.LabelColorOrange, 32 | LabelColorYellow: api.LabelColorYellow, 33 | LabelColorGreen: api.LabelColorGreen, 34 | LabelColorBlue: api.LabelColorBlue, 35 | LabelColorPurple: api.LabelColorPurple, 36 | LabelColorGray: api.LabelColorGray, 37 | } 38 | 39 | labelColorRevMap = map[string]LabelColor{ 40 | api.LabelColorBlank: LabelColorBlank, 41 | api.LabelColorRed: LabelColorRed, 42 | api.LabelColorOrange: LabelColorOrange, 43 | api.LabelColorYellow: LabelColorYellow, 44 | api.LabelColorGreen: LabelColorGreen, 45 | api.LabelColorBlue: LabelColorBlue, 46 | api.LabelColorPurple: LabelColorPurple, 47 | api.LabelColorGray: LabelColorGray, 48 | } 49 | ) 50 | 51 | type Label struct { 52 | Id string 53 | Name string 54 | Color LabelColor 55 | } 56 | 57 | func (l *Label) from(info *types.LabelInfo) *Label { 58 | l.Id = info.Id 59 | l.Name = info.Name 60 | l.Color = labelColorRevMap[info.Color] 61 | return l 62 | } 63 | 64 | type labelIterator struct { 65 | llc client.Client 66 | offset int 67 | limit int 68 | result *types.LabelListResult 69 | } 70 | 71 | func (i *labelIterator) update() (err error) { 72 | if i.result != nil && i.offset >= i.result.Total { 73 | return errNoMoreItems 74 | } 75 | spec := (&api.LabelListSpec{}).Init(i.offset, i.limit) 76 | if err = i.llc.CallApi(spec, context.Background()); err == nil { 77 | i.result = &spec.Result 78 | } 79 | return 80 | } 81 | 82 | func (i *labelIterator) Count() int { 83 | if i.result == nil { 84 | return 0 85 | } 86 | return i.result.Total 87 | } 88 | 89 | func (i *labelIterator) Items() iter.Seq2[int, *Label] { 90 | return func(yield func(int, *Label) bool) { 91 | for { 92 | for index, li := range i.result.List { 93 | if stop := !yield(i.offset+index, (&Label{}).from(li)); stop { 94 | return 95 | } 96 | } 97 | i.offset += i.limit 98 | if err := i.update(); err != nil { 99 | return 100 | } 101 | } 102 | } 103 | } 104 | 105 | func (a *Agent) LabelIterate() (it Iterator[Label], err error) { 106 | li := &labelIterator{ 107 | llc: a.llc, 108 | offset: 0, 109 | limit: protocol.LabelListLimit, 110 | } 111 | if err = li.update(); err == nil { 112 | it = li 113 | } 114 | return 115 | } 116 | 117 | // LabelFind finds label whose name is name, and returns it. 118 | func (a *Agent) LabelFind(name string, label *Label) (err error) { 119 | spec := (&api.LabelSearchSpec{}).Init(name, 0, protocol.LabelListLimit) 120 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 121 | return 122 | } 123 | 124 | if spec.Result.Total == 0 || spec.Result.List[0].Name != name { 125 | err = errors.ErrNotExist 126 | } else { 127 | li := spec.Result.List[0] 128 | label.Id = li.Id 129 | label.Name = li.Name 130 | label.Color = labelColorRevMap[li.Color] 131 | } 132 | return 133 | } 134 | 135 | // LabelCreate creates a label with name and color, returns its ID. 136 | func (a *Agent) LabelCreate(name string, color LabelColor) (labelId string, err error) { 137 | colorName, ok := labelColorMap[color] 138 | if !ok { 139 | colorName = api.LabelColorBlank 140 | } 141 | spec := (&api.LabelCreateSpec{}).Init( 142 | name, colorName, 143 | ) 144 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 145 | return 146 | } 147 | if len(spec.Result) > 0 { 148 | labelId = spec.Result[0].Id 149 | } 150 | return 151 | } 152 | 153 | // LabelUpdate updates label's name or color. 154 | func (a *Agent) LabelUpdate(label *Label) (err error) { 155 | if label == nil || label.Id == "" { 156 | return 157 | } 158 | colorName, ok := labelColorMap[label.Color] 159 | if !ok { 160 | colorName = api.LabelColorBlank 161 | } 162 | spec := (&api.LabelEditSpec{}).Init( 163 | label.Id, label.Name, colorName, 164 | ) 165 | return a.llc.CallApi(spec, context.Background()) 166 | } 167 | 168 | // LabelDelete deletes a label whose ID is labelId. 169 | func (a *Agent) LabelDelete(labelId string) (err error) { 170 | if labelId == "" { 171 | return 172 | } 173 | spec := (&api.LabelDeleteSpec{}).Init(labelId) 174 | return a.llc.CallApi(spec, context.Background()) 175 | } 176 | 177 | func (a *Agent) LabelSetOrder(labelId string, order FileOrder, asc bool) (err error) { 178 | spec := (&api.LabelSetOrderSpec{}).Init( 179 | labelId, getOrderName(order), asc, 180 | ) 181 | return a.llc.CallApi(spec, context.Background()) 182 | } 183 | 184 | // FileSetLabels sets labels for a file, you can also remove all labels from it 185 | // by not passing any labelId. 186 | func (a *Agent) FileSetLabels(fileId string, labelIds ...string) (err error) { 187 | spec := (&api.FileLabelSpec{}).Init(fileId, labelIds) 188 | return a.llc.CallApi(spec, context.Background()) 189 | } 190 | -------------------------------------------------------------------------------- /login.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deadblue/elevengo/internal/protocol" 7 | "github.com/deadblue/elevengo/lowlevel/api" 8 | ) 9 | 10 | // Credential contains required information which 115 server uses to 11 | // authenticate a signed-in user. 12 | // In detail, these cookies are required: "UID", "CID", "KID", "SEID". 13 | // Caller can find them from browser cookie storage. 14 | type Credential struct { 15 | UID string 16 | CID string 17 | KID string 18 | SEID string 19 | } 20 | 21 | // UserInfo contains the basic information of a signed-in user. 22 | type UserInfo struct { 23 | // User ID 24 | Id int 25 | // User name 26 | Name string 27 | // Is user VIP 28 | IsVip bool 29 | } 30 | 31 | // CredentialImport imports credentials into agent. 32 | func (a *Agent) CredentialImport(cr *Credential) (err error) { 33 | cookies := map[string]string{ 34 | protocol.CookieNameUID: cr.UID, 35 | protocol.CookieNameCID: cr.CID, 36 | protocol.CookieNameSEID: cr.SEID, 37 | } 38 | if cr.KID != "" { 39 | cookies[protocol.CookieNameKID] = cr.KID 40 | } 41 | a.llc.ImportCookies(cookies, protocol.CookieDomains...) 42 | return a.afterSignIn(cr.UID) 43 | } 44 | 45 | // CredentialExport exports current credentials for future-use. 46 | func (a *Agent) CredentialExport(cr *Credential) { 47 | cookies := a.llc.ExportCookies(protocol.CookieUrl) 48 | cr.UID = cookies[protocol.CookieNameUID] 49 | cr.CID = cookies[protocol.CookieNameCID] 50 | cr.KID = cookies[protocol.CookieNameKID] 51 | cr.SEID = cookies[protocol.CookieNameSEID] 52 | } 53 | 54 | func (a *Agent) afterSignIn(uid string) (err error) { 55 | // Call UploadInfo API to get userId and userKey 56 | spec := (&api.UploadInfoSpec{}).Init() 57 | if err = a.llc.CallApi(spec, context.Background()); err == nil { 58 | a.common.SetUserInfo(spec.Result.UserId, spec.Result.UserKey) 59 | a.isWeb = protocol.IsWebCredential(uid) 60 | } 61 | return 62 | } 63 | 64 | // UserGet get information of current signed-in user. 65 | func (a *Agent) UserGet(info *UserInfo) (err error) { 66 | spec := (&api.UserInfoSpec{}).Init() 67 | if err = a.llc.CallApi(spec, context.Background()); err == nil { 68 | info.Id = spec.Result.UserId 69 | info.Name = spec.Result.UserName 70 | info.IsVip = spec.Result.IsVip != 0 71 | } 72 | return 73 | } 74 | -------------------------------------------------------------------------------- /lowlevel.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/lowlevel/client" 5 | "github.com/deadblue/elevengo/lowlevel/types" 6 | ) 7 | 8 | // LowlevelClient returns low-level client that can be used to directly call ApiSpec. 9 | func (a *Agent) LowlevelClient() client.Client { 10 | return a.llc 11 | } 12 | 13 | // LowlevelParams returns common parameters for low-level API calling. 14 | func (a *Agent) LowlevelParams() types.CommonParams { 15 | return a.common 16 | } 17 | -------------------------------------------------------------------------------- /lowlevel/api/app.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/internal/protocol" 5 | "github.com/deadblue/elevengo/lowlevel/types" 6 | ) 7 | 8 | const ( 9 | AppAndroidLife = "Android" 10 | AppAndroidTv = "Android-tv" 11 | AppAndroidPan = "115wangpan_android" 12 | AppBrowserWindows = "PC-115chrome" 13 | AppBrowserMacOS = "MAC-115chrome" 14 | 15 | AppNameBrowser = "115Browser" 16 | // Deprecated 17 | //AppNameDesktop = "115Desktop" 18 | ) 19 | 20 | type AppVersionSpec struct { 21 | _JsonApiSpec[types.AppVersionResult, protocol.StandardResp] 22 | } 23 | 24 | func (s *AppVersionSpec) Init() *AppVersionSpec { 25 | s._JsonApiSpec.Init( 26 | "https://appversion.115.com/1/web/1.0/api/getMultiVer", 27 | ) 28 | return s 29 | } 30 | -------------------------------------------------------------------------------- /lowlevel/api/basic.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | ) 8 | 9 | /* 10 | |ApiSpec| inheritance tree: 11 | _____________|_BasicApiSpec|___________ 12 | / | \ 13 | |_JsonApiSpec| |_JsonpApiSpec| |_M115ApiSpec| 14 | / \ 15 | |_StandardApiSpec| |_VoidApiSpec| 16 | */ 17 | 18 | // _BasicApiSpec is the base struct for all |client.ApiSpec| implementations. 19 | type _BasicApiSpec struct { 20 | // Is crypto 21 | crypto bool 22 | 23 | // Base url 24 | baseUrl string 25 | 26 | // Query paramsters 27 | query util.Params 28 | } 29 | 30 | // Init initializes _BasicApiSpec, all child structs should call this method 31 | // before use. 32 | func (s *_BasicApiSpec) Init(baseUrl string) { 33 | s.baseUrl = baseUrl 34 | s.query = util.Params{} 35 | } 36 | 37 | // IsCrypto implements `ApiSpec.IsCrypto` 38 | func (s *_BasicApiSpec) IsCrypto() bool { 39 | return s.crypto 40 | } 41 | 42 | // SetCryptoKey implements `ApiSpec.SetCryptoKey` 43 | func (s *_BasicApiSpec) SetCryptoKey(key string) { 44 | s.query.Set("k_ec", key) 45 | } 46 | 47 | // Url implements `ApiSpec.Url` 48 | func (s *_BasicApiSpec) Url() string { 49 | if len(s.query) == 0 { 50 | return s.baseUrl 51 | } else { 52 | qs := s.query.Encode() 53 | if strings.ContainsRune(s.baseUrl, '?') { 54 | return s.baseUrl + "&" + qs 55 | } else { 56 | return s.baseUrl + "?" + qs 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lowlevel/api/dir.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/internal/protocol" 5 | ) 6 | 7 | type DirCreateSpec struct { 8 | _JsonApiSpec[string, protocol.DirCreateResp] 9 | } 10 | 11 | func (s *DirCreateSpec) Init(parentId, name string) *DirCreateSpec { 12 | s._JsonApiSpec.Init("https://webapi.115.com/files/add") 13 | s.form.Set("pid", parentId).Set("cname", name) 14 | return s 15 | } 16 | 17 | type DirSetOrderSpec struct { 18 | _VoidApiSpec 19 | } 20 | 21 | func (s *DirSetOrderSpec) Init(dirId string, order string, asc bool) *DirSetOrderSpec { 22 | s._VoidApiSpec.Init("https://webapi.115.com/files/order") 23 | s.form.Set("file_id", dirId). 24 | Set("fc_mix", "0"). 25 | Set("user_order", order) 26 | if asc { 27 | s.form.Set("user_asc", "1") 28 | } else { 29 | s.form.Set("user_asc", "0") 30 | } 31 | return s 32 | } 33 | 34 | type DirLocateSpec struct { 35 | _JsonApiSpec[string, protocol.DirLocateResp] 36 | } 37 | 38 | func (s *DirLocateSpec) Init(path string) *DirLocateSpec { 39 | s._JsonApiSpec.Init("https://webapi.115.com/files/getid") 40 | s.query.Set("path", path) 41 | return s 42 | } 43 | -------------------------------------------------------------------------------- /lowlevel/api/download.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/lowlevel/types" 5 | ) 6 | 7 | type DownloadSpec struct { 8 | _M115ApiSpec[types.DownloadResult] 9 | } 10 | 11 | func (s *DownloadSpec) Init(pickcode string) *DownloadSpec { 12 | s._M115ApiSpec.Init("https://proapi.115.com/app/chrome/downurl") 13 | s.query.SetNow("t") 14 | s.params.Set("pickcode", pickcode) 15 | return s 16 | } 17 | -------------------------------------------------------------------------------- /lowlevel/api/file.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "github.com/deadblue/elevengo/internal/protocol" 8 | "github.com/deadblue/elevengo/lowlevel/types" 9 | ) 10 | 11 | const ( 12 | FileOrderByName = "file_name" 13 | FileOrderBySize = "file_size" 14 | FileOrderByType = "file_type" 15 | FileOrderByCreateTime = "user_ptime" 16 | FileOrderByUpdateTime = "user_utime" 17 | FileOrderByOpenTime = "user_otime" 18 | FileOrderDefault = FileOrderByCreateTime 19 | ) 20 | 21 | type FileListSpec struct { 22 | _JsonApiSpec[types.FileListResult, protocol.FileListResp] 23 | 24 | // Save file order 25 | fo string 26 | } 27 | 28 | func (s *FileListSpec) Init(dirId string, offset, limit int) *FileListSpec { 29 | s._JsonApiSpec.Init("") 30 | s.query.Set("format", "json"). 31 | Set("aid", "1"). 32 | Set("cid", dirId). 33 | Set("show_dir", "1"). 34 | Set("fc_mix", "0"). 35 | SetInt("offset", offset). 36 | SetInt("limit", limit). 37 | Set("o", FileOrderDefault). 38 | Set("asc", "0") 39 | // s.QuerySet("snap", "0") 40 | // s.QuerySet("natsort", "1") 41 | return s 42 | } 43 | 44 | func (s *FileListSpec) Url() string { 45 | // Select base URL 46 | if s.fo == FileOrderByName { 47 | s.baseUrl = "https://aps.115.com/natsort/files.php" 48 | } else { 49 | s.baseUrl = "https://webapi.115.com/files" 50 | } 51 | return s._JsonApiSpec.Url() 52 | } 53 | 54 | func (s *FileListSpec) SetOrder(order string, asc int) { 55 | s.fo = order 56 | s.query.Set("o", order).SetInt("asc", asc) 57 | } 58 | 59 | func (s *FileListSpec) SetStared() { 60 | s.query.Set("star", "1") 61 | } 62 | 63 | func (s *FileListSpec) SetFileType(fileType int) { 64 | if fileType != 0 { 65 | s.query.SetInt("type", fileType) 66 | } 67 | } 68 | 69 | func (s *FileListSpec) SetFileExtension(extName string) { 70 | if extName != "" { 71 | s.query.Set("suffix", extName) 72 | } 73 | } 74 | 75 | type FileSearchSpec struct { 76 | _JsonApiSpec[types.FileListResult, protocol.FileSearchResp] 77 | } 78 | 79 | func (s *FileSearchSpec) Init(offset, limit int) *FileSearchSpec { 80 | s._JsonApiSpec.Init("https://webapi.115.com/files/search") 81 | s.query.Set("aid", "1"). 82 | Set("show_dir", "1"). 83 | SetInt("offset", offset). 84 | SetInt("limit", limit). 85 | Set("format", "json") 86 | return s 87 | } 88 | 89 | func (s *FileSearchSpec) ByKeyword(dirId, keyword string) { 90 | s.query.Set("cid", dirId).Set("search_value", keyword) 91 | } 92 | 93 | func (s *FileSearchSpec) ByLabelId(labelId string) { 94 | s.query.Set("cid", "0").Set("file_label", labelId) 95 | } 96 | 97 | func (s *FileSearchSpec) SetFileType(fileType int) { 98 | if fileType > 0 { 99 | s.query.SetInt("type", fileType) 100 | } 101 | } 102 | 103 | func (s *FileSearchSpec) SetFileExtension(extName string) { 104 | if extName != "" { 105 | s.query.Set("suffix", extName) 106 | } 107 | } 108 | 109 | type FileGetSpec struct { 110 | _JsonApiSpec[types.FileGetResult, protocol.StandardResp] 111 | } 112 | 113 | func (s *FileGetSpec) Init(fileId string) *FileGetSpec { 114 | s._JsonApiSpec.Init("https://webapi.115.com/files/get_info") 115 | s.query.Set("file_id", fileId) 116 | return s 117 | } 118 | 119 | type FileRenameSpec struct { 120 | _VoidApiSpec 121 | } 122 | 123 | func (s *FileRenameSpec) Init() *FileRenameSpec { 124 | s._VoidApiSpec.Init("https://webapi.115.com/files/batch_rename") 125 | return s 126 | } 127 | 128 | func (s *FileRenameSpec) Add(fileId, newName string) { 129 | key := fmt.Sprintf("files_new_name[%s]", fileId) 130 | s.form.Set(key, newName) 131 | } 132 | 133 | type FileMoveSpec struct { 134 | _VoidApiSpec 135 | } 136 | 137 | func (s *FileMoveSpec) Init(dirId string, fileIds []string) *FileMoveSpec { 138 | s._VoidApiSpec.Init("https://webapi.115.com/files/move") 139 | s.form.Set("pid", dirId) 140 | for i, fileId := range fileIds { 141 | key := fmt.Sprintf("fid[%d]", i) 142 | s.form.Set(key, fileId) 143 | } 144 | return s 145 | } 146 | 147 | type FileCopySpec struct { 148 | _VoidApiSpec 149 | } 150 | 151 | func (s *FileCopySpec) Init(dirId string, fileIds []string) *FileCopySpec { 152 | s._VoidApiSpec.Init("https://webapi.115.com/files/copy") 153 | s.form.Set("pid", dirId) 154 | for i, fileId := range fileIds { 155 | key := fmt.Sprintf("fid[%d]", i) 156 | s.form.Set(key, fileId) 157 | } 158 | return s 159 | } 160 | 161 | type FileDeleteSpec struct { 162 | _VoidApiSpec 163 | } 164 | 165 | func (s *FileDeleteSpec) Init(fileIds []string) *FileDeleteSpec { 166 | s._VoidApiSpec.Init("https://webapi.115.com/rb/delete") 167 | s.form.Set("ignore_warn", "1") 168 | for i, fileId := range fileIds { 169 | key := fmt.Sprintf("fid[%d]", i) 170 | s.form.Set(key, fileId) 171 | } 172 | return s 173 | } 174 | 175 | type FileStarSpec struct { 176 | _VoidApiSpec 177 | } 178 | 179 | func (s *FileStarSpec) Init(fileId string, star bool) *FileStarSpec { 180 | s._VoidApiSpec.Init("https://webapi.115.com/files/star") 181 | s.form.Set("file_id", fileId) 182 | if star { 183 | s.form.Set("star", "1") 184 | } else { 185 | s.form.Set("star", "0") 186 | } 187 | return s 188 | } 189 | 190 | type FileLabelSpec struct { 191 | _VoidApiSpec 192 | } 193 | 194 | func (s *FileLabelSpec) Init(fileId string, labelIds []string) *FileLabelSpec { 195 | s._VoidApiSpec.Init("https://webapi.115.com/files/edit") 196 | s.form.Set("fid", fileId) 197 | if len(labelIds) == 0 { 198 | s.form.Set("file_label", "") 199 | } else { 200 | s.form.Set("file_label", strings.Join(labelIds, ",")) 201 | } 202 | return s 203 | } 204 | 205 | type FileSetDescSpec struct { 206 | _VoidApiSpec 207 | } 208 | 209 | func (s *FileSetDescSpec) Init(fileId string, desc string) *FileSetDescSpec { 210 | s._VoidApiSpec.Init("https://webapi.115.com/files/edit") 211 | s.form.Set("fid", fileId). 212 | Set("file_desc", desc) 213 | return s 214 | } 215 | 216 | type FileGetDescSpec struct { 217 | _JsonApiSpec[string, protocol.FileGetDescResp] 218 | } 219 | 220 | func (s *FileGetDescSpec) Init(fileId string) *FileGetDescSpec { 221 | s._JsonApiSpec.Init("https://webapi.115.com/files/desc") 222 | s.query.Set("file_id", fileId). 223 | Set("format", "json"). 224 | Set("compat", "1"). 225 | Set("new_html", "1") 226 | return s 227 | } 228 | 229 | type FileHideSpec struct { 230 | _VoidApiSpec 231 | } 232 | 233 | func (s *FileHideSpec) Init(hide bool, fileIds []string) *FileHideSpec { 234 | s._VoidApiSpec.Init("https://webapi.115.com/files/hiddenfiles") 235 | for i, fileId := range fileIds { 236 | key := fmt.Sprintf("fid[%d]", i) 237 | s.form.Set(key, fileId) 238 | } 239 | if hide { 240 | s.form.Set("hidden", "1") 241 | } else { 242 | s.form.Set("hidden", "0") 243 | } 244 | return s 245 | } 246 | -------------------------------------------------------------------------------- /lowlevel/api/hidden.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type ShowHiddenSpec struct { 4 | _VoidApiSpec 5 | } 6 | 7 | func (s *ShowHiddenSpec) Init(password string) *ShowHiddenSpec { 8 | s._VoidApiSpec.Init("https://115.com/?ct=hiddenfiles&ac=switching") 9 | s.form.Set("show", "1"). 10 | Set("valid_type", "1"). 11 | Set("safe_pwd", password) 12 | return s 13 | } 14 | 15 | type HideHiddenSpec struct { 16 | _VoidApiSpec 17 | } 18 | 19 | func (s *HideHiddenSpec) Init() *HideHiddenSpec { 20 | s._VoidApiSpec.Init("https://115.com/?ct=hiddenfiles&ac=switching") 21 | s.form.Set("show", "0"). 22 | Set("valid_type", "1"). 23 | Set("safe_pwd", "") 24 | return s 25 | } 26 | -------------------------------------------------------------------------------- /lowlevel/api/image.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/deadblue/elevengo/lowlevel/types" 4 | 5 | type ImageGetSpec struct { 6 | _StandardApiSpec[types.ImageGetResult] 7 | } 8 | 9 | func (s *ImageGetSpec) Init(pickcode string) *ImageGetSpec { 10 | s._StandardApiSpec.Init("https://webapi.115.com/files/image") 11 | s.query.Set("pickcode", pickcode) 12 | s.query.SetNow("_") 13 | return s 14 | } 15 | -------------------------------------------------------------------------------- /lowlevel/api/index.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/deadblue/elevengo/lowlevel/types" 4 | 5 | type IndexInfoSpec struct { 6 | _StandardApiSpec[types.IndexInfoResult] 7 | } 8 | 9 | func (s *IndexInfoSpec) Init() *IndexInfoSpec { 10 | s._StandardApiSpec.Init("https://webapi.115.com/files/index_info") 11 | return s 12 | } 13 | -------------------------------------------------------------------------------- /lowlevel/api/json.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/deadblue/elevengo/internal/impl" 9 | "github.com/deadblue/elevengo/internal/protocol" 10 | "github.com/deadblue/elevengo/internal/util" 11 | "github.com/deadblue/elevengo/lowlevel/client" 12 | "github.com/deadblue/elevengo/lowlevel/types" 13 | ) 14 | 15 | type _ApiResp interface { 16 | // Err returns an error if API calling failed. 17 | Err() error 18 | } 19 | 20 | type _GenericDataApiResp[D any] interface { 21 | // Extract extracts result from response to |v|. 22 | Extract(v *D) error 23 | } 24 | 25 | type _DataApiResp interface { 26 | // Extract extracts result from response to |v|. 27 | Extract(v any) error 28 | } 29 | 30 | // _JsonApiSpec is the base spec for all JSON ApiSpec. 31 | // 32 | // Type parameters: 33 | // - D: Result type. 34 | // - R: Response type. 35 | type _JsonApiSpec[D, R any] struct { 36 | _BasicApiSpec 37 | 38 | // Request parameters in form 39 | form util.Params 40 | 41 | // The API result, its value will be filled after |Parse| called. 42 | Result D 43 | } 44 | 45 | func (s *_JsonApiSpec[D, R]) Init(baseUrl string) { 46 | s._BasicApiSpec.Init(baseUrl) 47 | s.form = util.Params{} 48 | } 49 | 50 | func (s *_JsonApiSpec[D, R]) Payload() client.Payload { 51 | if len(s.form) == 0 { 52 | return nil 53 | } else { 54 | return impl.WwwFormPayload(s.form.Encode()) 55 | } 56 | } 57 | 58 | func (s *_JsonApiSpec[D, R]) Parse(r io.Reader) (err error) { 59 | jd, resp := json.NewDecoder(r), any(new(R)) 60 | if err = jd.Decode(resp); err != nil { 61 | return 62 | } 63 | // Check response error 64 | ar := resp.(_ApiResp) 65 | if err = ar.Err(); err != nil { 66 | return 67 | } 68 | // Extract data 69 | if gdr, ok := resp.(_GenericDataApiResp[D]); ok { 70 | err = gdr.Extract(&s.Result) 71 | } else if dr, ok := resp.(_DataApiResp); ok { 72 | err = dr.Extract(&s.Result) 73 | } 74 | return 75 | } 76 | 77 | // _JsonpApiSpec is the base spec for all JSON-P ApiSpec. 78 | // 79 | // Type parameters: 80 | // - D: Result type. 81 | // - R: Response type. 82 | // 83 | //lint:ignore U1000 Remain for future-use. 84 | type _JsonpApiSpec[D, R any] struct { 85 | _BasicApiSpec 86 | 87 | // The API result, its value will be filled after |Parse| called. 88 | Result D 89 | } 90 | 91 | func (s *_JsonpApiSpec[D, R]) Init(baseUrl, cb string) { 92 | s._BasicApiSpec.Init(baseUrl) 93 | s.query.Set("callback", cb) 94 | } 95 | 96 | func (s *_JsonpApiSpec[D, R]) Payload() client.Payload { 97 | return nil 98 | } 99 | 100 | func (s *_JsonpApiSpec[D, R]) Parse(r io.Reader) (err error) { 101 | // Read response 102 | var body []byte 103 | if body, err = io.ReadAll(r); err != nil { 104 | return 105 | } 106 | // Find JSON content 107 | left, right := bytes.IndexByte(body, '('), bytes.LastIndexByte(body, ')') 108 | if left < 0 || right < 0 { 109 | return &json.SyntaxError{Offset: 0} 110 | } 111 | // Parse JSON 112 | resp := any(new(R)) 113 | if err = json.Unmarshal(body[left+1:right], resp); err != nil { 114 | return 115 | } 116 | // Force convert resp to ApiResp 117 | ar := resp.(_ApiResp) 118 | if err = ar.Err(); err != nil { 119 | return 120 | } 121 | // Extract data 122 | if gdr, ok := resp.(_GenericDataApiResp[D]); ok { 123 | err = gdr.Extract(&s.Result) 124 | } else if dr, ok := resp.(_DataApiResp); ok { 125 | err = dr.Extract(&s.Result) 126 | } 127 | return 128 | } 129 | 130 | // _StandardApiSpec is the base spec for all standard JSON API specs. 131 | // 132 | // Type parameters: 133 | // - D: Result type. 134 | type _StandardApiSpec[D any] struct { 135 | _JsonApiSpec[D, protocol.StandardResp] 136 | } 137 | 138 | // _VoidApiSpec is the base spec for all JSON API specs which has no result. 139 | type _VoidApiSpec struct { 140 | _JsonApiSpec[types.VoidResult, protocol.BasicResp] 141 | } 142 | -------------------------------------------------------------------------------- /lowlevel/api/label.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/deadblue/elevengo/lowlevel/types" 4 | 5 | const ( 6 | LabelColorBlank = "#000000" 7 | LabelColorRed = "#FF4B30" 8 | LabelColorOrange = "#F78C26" 9 | LabelColorYellow = "#FFC032" 10 | LabelColorGreen = "#43BA80" 11 | LabelColorBlue = "#2670FC" 12 | LabelColorPurple = "#8B69FE" 13 | LabelColorGray = "#CCCCCC" 14 | ) 15 | 16 | type LabelListSpec struct { 17 | _StandardApiSpec[types.LabelListResult] 18 | } 19 | 20 | func (s *LabelListSpec) Init(offset, limit int) *LabelListSpec { 21 | s._StandardApiSpec.Init("https://webapi.115.com/label/list") 22 | s.query.SetInt("offset", offset) 23 | s.query.SetInt("limit", limit) 24 | // s.query.Set("sort", "create_time") 25 | // s.query.Set("order", "asc") 26 | return s 27 | } 28 | 29 | type LabelSearchSpec struct { 30 | _StandardApiSpec[types.LabelListResult] 31 | } 32 | 33 | func (s *LabelSearchSpec) Init(keyword string, offset, limit int) *LabelSearchSpec { 34 | s._StandardApiSpec.Init("https://webapi.115.com/label/list") 35 | s.query.Set("keyword", keyword) 36 | s.query.SetInt("offset", offset) 37 | s.query.SetInt("limit", limit) 38 | return s 39 | } 40 | 41 | type LabelCreateSpec struct { 42 | _StandardApiSpec[types.LabelCreateResult] 43 | } 44 | 45 | func (s *LabelCreateSpec) Init(name, color string) *LabelCreateSpec { 46 | s._StandardApiSpec.Init("https://webapi.115.com/label/add_multi") 47 | s.form.Set("name[]", name+"\x07"+color) 48 | return s 49 | } 50 | 51 | type LabelEditSpec struct { 52 | _VoidApiSpec 53 | } 54 | 55 | func (s *LabelEditSpec) Init(labelId, name, color string) *LabelEditSpec { 56 | s._VoidApiSpec.Init("https://webapi.115.com/label/edit") 57 | s.form.Set("id", labelId). 58 | Set("name", name). 59 | Set("color", color) 60 | return s 61 | } 62 | 63 | type LabelDeleteSpec struct { 64 | _VoidApiSpec 65 | } 66 | 67 | func (s *LabelDeleteSpec) Init(labelId string) *LabelDeleteSpec { 68 | s._VoidApiSpec.Init("https://webapi.115.com/label/delete") 69 | s.form.Set("id", labelId) 70 | return s 71 | } 72 | 73 | type LabelSetOrderSpec struct { 74 | _VoidApiSpec 75 | } 76 | 77 | func (s *LabelSetOrderSpec) Init(labelId string, order string, asc bool) *LabelSetOrderSpec { 78 | s._VoidApiSpec.Init("https://webapi.115.com/files/order") 79 | s.form.Set("module", "label_search"). 80 | Set("file_id", labelId). 81 | Set("fc_mix", "0"). 82 | Set("user_order", order) 83 | if asc { 84 | s.form.Set("user_asc", "1") 85 | } else { 86 | s.form.Set("user_asc", "0") 87 | } 88 | return s 89 | } 90 | -------------------------------------------------------------------------------- /lowlevel/api/m115.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/url" 7 | 8 | "github.com/deadblue/elevengo/internal/crypto/m115" 9 | "github.com/deadblue/elevengo/internal/impl" 10 | "github.com/deadblue/elevengo/internal/protocol" 11 | "github.com/deadblue/elevengo/internal/util" 12 | "github.com/deadblue/elevengo/lowlevel/client" 13 | ) 14 | 15 | type _M115Result interface { 16 | UnmarshalResult([]byte) error 17 | } 18 | 19 | // _M115ApiSpec is the base API spec for all m115 encoded ApiSpec. 20 | type _M115ApiSpec[D any] struct { 21 | _BasicApiSpec 22 | 23 | // Cipher key for encrypt/decrypt. 24 | key m115.Key 25 | // API parameters. 26 | params util.Params 27 | 28 | // Final result. 29 | Result D 30 | } 31 | 32 | func (s *_M115ApiSpec[D]) Init(baseUrl string) { 33 | s._BasicApiSpec.Init(baseUrl) 34 | s.key = m115.GenerateKey() 35 | s.params = util.Params{} 36 | } 37 | 38 | // Payload implements |ApiSpec.Payload|. 39 | func (s *_M115ApiSpec[D]) Payload() client.Payload { 40 | data, err := json.Marshal(s.params) 41 | if err != nil { 42 | return nil 43 | } 44 | form := url.Values{} 45 | form.Set("data", m115.Encode(data, s.key)) 46 | return impl.WwwFormPayload(form.Encode()) 47 | } 48 | 49 | // Parse implements |ApiSpec.Parse|. 50 | func (s *_M115ApiSpec[D]) Parse(r io.Reader) (err error) { 51 | jd, resp := json.NewDecoder(r), &protocol.StandardResp{} 52 | if err = jd.Decode(resp); err != nil { 53 | return 54 | } 55 | if err = resp.Err(); err != nil { 56 | return err 57 | } 58 | // Decode response data 59 | var data string 60 | if err = resp.Extract(&data); err != nil { 61 | return 62 | } 63 | if result, err := m115.Decode(data, s.key); err == nil { 64 | ptr := any(&s.Result) 65 | if mr, ok := ptr.(_M115Result); ok { 66 | return mr.UnmarshalResult(result) 67 | } else { 68 | return json.Unmarshal(result, ptr) 69 | } 70 | } else { 71 | return err 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /lowlevel/api/offline.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/deadblue/elevengo/internal/protocol" 7 | "github.com/deadblue/elevengo/lowlevel/types" 8 | ) 9 | 10 | type OfflineListSpec struct { 11 | _JsonApiSpec[types.OfflineListResult, protocol.OfflineListResp] 12 | } 13 | 14 | func (s *OfflineListSpec) Init(page int) *OfflineListSpec { 15 | s._JsonApiSpec.Init("https://lixian.115.com/lixian/?ct=lixian&ac=task_lists") 16 | s.query.SetInt("page", page) 17 | return s 18 | } 19 | 20 | type OfflineDeleteSpec struct { 21 | _VoidApiSpec 22 | } 23 | 24 | func (s *OfflineDeleteSpec) Init(hashes []string, deleteFiles bool) *OfflineDeleteSpec { 25 | s._VoidApiSpec.Init("https://lixian.115.com/lixian/?ct=lixian&ac=task_del") 26 | for index, hash := range hashes { 27 | key := fmt.Sprintf("hash[%d]", index) 28 | s.form.Set(key, hash) 29 | } 30 | if deleteFiles { 31 | s.form.Set("flag", "1") 32 | } else { 33 | s.form.Set("flag", "0") 34 | } 35 | return s 36 | } 37 | 38 | type OfflineClearSpec struct { 39 | _VoidApiSpec 40 | } 41 | 42 | func (s *OfflineClearSpec) Init(flag int) *OfflineClearSpec { 43 | s._VoidApiSpec.Init("https://lixian.115.com/lixian/?ct=lixian&ac=task_clear") 44 | s.form.SetInt("flag", flag) 45 | return s 46 | } 47 | 48 | type OfflineAddUrlsSpec struct { 49 | _M115ApiSpec[types.OfflineAddUrlsResult] 50 | } 51 | 52 | func (s *OfflineAddUrlsSpec) Init(urls []string, saveDirId string, common *types.CommonParams) *OfflineAddUrlsSpec { 53 | s._M115ApiSpec.Init("https://lixian.115.com/lixianssp/?ac=add_task_urls") 54 | s.crypto = true 55 | s.params.Set("app_ver", common.AppVer). 56 | Set("uid", common.UserId). 57 | Set("ac", "add_task_urls") 58 | for i, url := range urls { 59 | key := fmt.Sprintf("url[%d]", i) 60 | s.params.Set(key, url) 61 | } 62 | if saveDirId != "" { 63 | s.params.Set("wp_path_id", saveDirId) 64 | } 65 | return s 66 | } 67 | -------------------------------------------------------------------------------- /lowlevel/api/qrcode.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/deadblue/elevengo/internal/protocol" 7 | "github.com/deadblue/elevengo/lowlevel/types" 8 | ) 9 | 10 | const ( 11 | qrcodeTokenUrl = "https://qrcodeapi.115.com/api/1.0/%s/1.0/token" 12 | qrcodeLoginUrl = "https://passportapi.115.com/app/1.0/%s/1.0/login/qrcode" 13 | 14 | qrcodeImageUrl = "https://qrcodeapi.115.com/api/1.0/web/1.0/qrcode?qrfrom=1&client=0&uid=%s" 15 | ) 16 | 17 | type QrcodeTokenSpec struct { 18 | _JsonApiSpec[types.QrcodeTokenResult, protocol.QrcodeBaseResp] 19 | } 20 | 21 | func (s *QrcodeTokenSpec) Init(app string) *QrcodeTokenSpec { 22 | s._JsonApiSpec.Init(fmt.Sprintf(qrcodeTokenUrl, app)) 23 | return s 24 | } 25 | 26 | type QrcodeStatusSpec struct { 27 | _JsonApiSpec[types.QrcodeStatusResult, protocol.QrcodeBaseResp] 28 | } 29 | 30 | func (s *QrcodeStatusSpec) Init(uid string, time int64, sign string) *QrcodeStatusSpec { 31 | s._JsonApiSpec.Init("https://qrcodeapi.115.com/get/status/") 32 | s.query.Set("uid", uid). 33 | SetInt64("time", time). 34 | Set("sign", sign). 35 | SetNow("_") 36 | return s 37 | } 38 | 39 | type QrcodeLoginSpec struct { 40 | _JsonApiSpec[types.QrcodeLoginResult, protocol.QrcodeBaseResp] 41 | } 42 | 43 | func (s *QrcodeLoginSpec) Init(app, uid string) *QrcodeLoginSpec { 44 | s._JsonApiSpec.Init(fmt.Sprintf(qrcodeLoginUrl, app)) 45 | s.form.Set("account", uid). 46 | Set("app", app) 47 | return s 48 | } 49 | 50 | func QrcodeImageUrl(uid string) string { 51 | return fmt.Sprintf(qrcodeImageUrl, uid) 52 | } 53 | -------------------------------------------------------------------------------- /lowlevel/api/recycle.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/internal/protocol" 5 | "github.com/deadblue/elevengo/lowlevel/types" 6 | ) 7 | 8 | type RecycleBinListSpec struct { 9 | _JsonApiSpec[types.RecycleBinListResult, protocol.RecycleBinListResp] 10 | } 11 | 12 | func (s *RecycleBinListSpec) Init(offset, limit int) *RecycleBinListSpec { 13 | s._JsonApiSpec.Init("https://webapi.115.com/rb") 14 | s.query.Set("aid", "7"). 15 | Set("cid", "0"). 16 | Set("format", "json"). 17 | SetInt("offset", offset). 18 | SetInt("limit", limit) 19 | return s 20 | } 21 | 22 | type RecycleBinCleanSpec struct { 23 | _VoidApiSpec 24 | } 25 | 26 | func (s *RecycleBinCleanSpec) Init(password string) *RecycleBinCleanSpec { 27 | s._VoidApiSpec.Init("https://webapi.115.com/rb/clean") 28 | s.form.Set("password", password) 29 | return s 30 | } 31 | -------------------------------------------------------------------------------- /lowlevel/api/share.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/deadblue/elevengo/internal/protocol" 7 | "github.com/deadblue/elevengo/lowlevel/types" 8 | ) 9 | 10 | type ShareDuration int 11 | 12 | const ( 13 | ShareOneDay ShareDuration = 1 14 | ShareOneWeek ShareDuration = 7 15 | ShareForever ShareDuration = -1 16 | ) 17 | 18 | type ShareListSpec struct { 19 | _JsonApiSpec[types.ShareListResult, protocol.ShareListResp] 20 | } 21 | 22 | func (s *ShareListSpec) Init(userId string, offset, limit int) *ShareListSpec { 23 | s._JsonApiSpec.Init("https://webapi.115.com/share/slist") 24 | s.query.Set("user_id", userId). 25 | SetInt("offset", offset). 26 | SetInt("limit", limit) 27 | return s 28 | } 29 | 30 | type ShareSendSpec struct { 31 | _StandardApiSpec[types.ShareInfo] 32 | } 33 | 34 | func (s *ShareSendSpec) Init(fileIds []string, userId string) *ShareSendSpec { 35 | s._StandardApiSpec.Init("https://webapi.115.com/share/send") 36 | s.form.Set("user_id", userId). 37 | Set("ignore_warn", "1"). 38 | Set("file_ids", strings.Join(fileIds, ",")) 39 | return s 40 | } 41 | 42 | type ShareGetSpec struct { 43 | _StandardApiSpec[types.ShareInfo] 44 | } 45 | 46 | func (s *ShareGetSpec) Init(shareCode string) *ShareGetSpec { 47 | s._StandardApiSpec.Init("https://webapi.115.com/share/shareinfo") 48 | s.query.Set("share_code", shareCode) 49 | return s 50 | } 51 | 52 | type ShareUpdateSpec struct { 53 | _VoidApiSpec 54 | } 55 | 56 | func (s *ShareUpdateSpec) Init( 57 | shareCode string, receiveCode string, duration ShareDuration, 58 | ) *ShareUpdateSpec { 59 | s._VoidApiSpec.Init("https://webapi.115.com/share/updateshare") 60 | s.form.Set("share_code", shareCode) 61 | if receiveCode == "" { 62 | s.form.Set("auto_fill_recvcode", "1") 63 | } else { 64 | s.form.Set("receive_code", receiveCode) 65 | } 66 | if duration > 0 { 67 | s.form.SetInt("share_duration", int(duration)) 68 | } 69 | return s 70 | } 71 | 72 | type ShareCancelSpec struct { 73 | _VoidApiSpec 74 | } 75 | 76 | func (s *ShareCancelSpec) Init(shareCode string) *ShareCancelSpec { 77 | s._VoidApiSpec.Init("https://webapi.115.com/share/updateshare") 78 | s.form.Set("share_code", shareCode) 79 | s.form.Set("action", "cancel") 80 | return s 81 | } 82 | 83 | type ShareSnapSpec struct { 84 | _JsonApiSpec[types.ShareSnapResult, protocol.ShareSnapResp] 85 | } 86 | 87 | func (s *ShareSnapSpec) Init( 88 | shareCode, receiveCode string, offset, limit int, dirId string, 89 | ) *ShareSnapSpec { 90 | s._JsonApiSpec.Init("https://webapi.115.com/share/snap") 91 | s.query.Set("share_code", shareCode). 92 | Set("receive_code", receiveCode). 93 | Set("cid", dirId). 94 | SetInt("offset", offset). 95 | SetInt("limit", limit) 96 | return s 97 | } 98 | 99 | type ShareReceiveSpec struct { 100 | _VoidApiSpec 101 | } 102 | 103 | func (s *ShareReceiveSpec) Init( 104 | userId, shareCode, receiveCode string, 105 | fileIds []string, receiveDirId string, 106 | ) *ShareReceiveSpec { 107 | s._VoidApiSpec.Init("https://webapi.115.com/share/receive") 108 | s.form.Set("user_id", userId). 109 | Set("share_code", shareCode). 110 | Set("receive_code", receiveCode). 111 | Set("file_id", strings.Join(fileIds, ",")) 112 | if receiveDirId != "" { 113 | s.form.Set("cid", receiveDirId) 114 | } 115 | return s 116 | } 117 | -------------------------------------------------------------------------------- /lowlevel/api/shortcut.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/deadblue/elevengo/lowlevel/types" 4 | 5 | type ShortcutListSpec struct { 6 | _StandardApiSpec[types.ShortcutListResult] 7 | } 8 | 9 | func (s *ShortcutListSpec) Init() *ShortcutListSpec { 10 | s._StandardApiSpec.Init("https://webapi.115.com/category/shortcut") 11 | return s 12 | } 13 | 14 | type ShortcutAddSpec struct { 15 | _VoidApiSpec 16 | } 17 | 18 | func (s *ShortcutAddSpec) Init(fileId string) *ShortcutAddSpec { 19 | s._VoidApiSpec.Init("https://webapi.115.com/category/shortcut") 20 | s.form.Set("file_id", fileId).Set("op", "add") 21 | return s 22 | } 23 | 24 | type ShortcutDeleteSpec struct { 25 | _VoidApiSpec 26 | } 27 | 28 | func (s *ShortcutDeleteSpec) Init(fileId string) *ShortcutDeleteSpec { 29 | s._VoidApiSpec.Init("https://webapi.115.com/category/shortcut") 30 | s.form.Set("file_id", fileId).Set("op", "delete") 31 | return s 32 | } 33 | -------------------------------------------------------------------------------- /lowlevel/api/upload.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "strings" 7 | "time" 8 | 9 | "github.com/deadblue/elevengo/internal/multipart" 10 | "github.com/deadblue/elevengo/internal/protocol" 11 | "github.com/deadblue/elevengo/internal/upload" 12 | "github.com/deadblue/elevengo/lowlevel/client" 13 | "github.com/deadblue/elevengo/lowlevel/types" 14 | ) 15 | 16 | const ( 17 | UploadMaxSizeSample = 200 * 1024 * 1024 18 | UploadMaxSize = 5 * 1024 * 1024 * 1024 19 | UploadMaxSizeOss = 115 * 1024 * 1024 * 1024 20 | ) 21 | 22 | func getTarget(dirId string) string { 23 | return fmt.Sprintf("U_1_%s", dirId) 24 | } 25 | 26 | type UploadInfoSpec struct { 27 | _JsonApiSpec[types.UploadInfoResult, protocol.UploadInfoResp] 28 | } 29 | 30 | func (s *UploadInfoSpec) Init() *UploadInfoSpec { 31 | s._JsonApiSpec.Init("https://proapi.115.com/app/uploadinfo") 32 | return s 33 | } 34 | 35 | type UploadInitSpec struct { 36 | _JsonApiSpec[types.UploadInitResult, protocol.UploadInitResp] 37 | } 38 | 39 | func (s *UploadInitSpec) Init( 40 | dirId string, fileSha1 string, fileName string, fileSize int64, 41 | signKey string, signValue string, 42 | common *types.CommonParams, 43 | ) *UploadInitSpec { 44 | s._JsonApiSpec.Init("https://uplb.115.com/4.0/initupload.php") 45 | s.crypto = true 46 | // Make sure fileSha1 and signValue are in upper-case. 47 | fileSha1 = strings.ToUpper(fileSha1) 48 | signValue = strings.ToUpper(signValue) 49 | // Prepare parameters 50 | target := getTarget(dirId) 51 | timestamp := time.Now().UnixMilli() 52 | signature := upload.CalcSignature(common.UserId, common.UserKey, fileSha1, target) 53 | token := upload.CalcToken( 54 | common.AppVer, common.UserId, common.UserHash, 55 | fileSha1, fileSize, signKey, signValue, timestamp, 56 | ) 57 | s.form.Set("appid", "0"). 58 | Set("appversion", common.AppVer). 59 | Set("userid", common.UserId). 60 | Set("filename", fileName). 61 | SetInt64("filesize", fileSize). 62 | Set("fileid", fileSha1). 63 | Set("target", target). 64 | Set("sig", signature). 65 | SetInt64("t", timestamp). 66 | Set("token", token) 67 | if signKey != "" && signValue != "" { 68 | s.form.Set("sign_key", signKey). 69 | Set("sign_val", signValue) 70 | } 71 | return s 72 | } 73 | 74 | type UploadTokenSpec struct { 75 | _JsonApiSpec[types.UploadTokenResult, protocol.UploadTokenResp] 76 | } 77 | 78 | func (s *UploadTokenSpec) Init() *UploadTokenSpec { 79 | s._JsonApiSpec.Init("https://uplb.115.com/3.0/gettoken.php") 80 | return s 81 | } 82 | 83 | type UploadSampleInitSpec struct { 84 | _JsonApiSpec[types.UploadSampleInitResult, protocol.UploadSampleInitResp] 85 | } 86 | 87 | func (s *UploadSampleInitSpec) Init( 88 | dirId string, fileName string, fileSize int64, 89 | common *types.CommonParams, 90 | ) *UploadSampleInitSpec { 91 | s._JsonApiSpec.Init("https://uplb.115.com/3.0/sampleinitupload.php") 92 | s.form.Set("userid", common.UserId). 93 | Set("filename", fileName). 94 | SetInt64("filesize", fileSize). 95 | Set("target", getTarget(dirId)) 96 | return s 97 | } 98 | 99 | type UploadSampleSpec struct { 100 | _StandardApiSpec[types.UploadSampleResult] 101 | payload client.Payload 102 | } 103 | 104 | func (s *UploadSampleSpec) Init( 105 | dirId, fileName string, fileSize int64, r io.Reader, 106 | initResult *types.UploadSampleInitResult, 107 | ) *UploadSampleSpec { 108 | s._StandardApiSpec.Init(initResult.Host) 109 | //Prepart payload 110 | s.payload = multipart.Builder(). 111 | AddValue("success_action_status", "200"). 112 | AddValue("name", fileName). 113 | AddValue("target", getTarget(dirId)). 114 | AddValue("key", initResult.Object). 115 | AddValue("policy", initResult.Policy). 116 | AddValue("OSSAccessKeyId", initResult.AccessKeyId). 117 | AddValue("callback", initResult.Callback). 118 | AddValue("signature", initResult.Signature). 119 | AddFile("file", fileName, fileSize, r). 120 | Build() 121 | return s 122 | } 123 | 124 | func (s *UploadSampleSpec) Payload() client.Payload { 125 | return s.payload 126 | } 127 | -------------------------------------------------------------------------------- /lowlevel/api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import "github.com/deadblue/elevengo/lowlevel/types" 4 | 5 | type UserInfoSpec struct { 6 | _StandardApiSpec[types.UserInfoResult] 7 | } 8 | 9 | func (s *UserInfoSpec) Init() *UserInfoSpec { 10 | s._StandardApiSpec.Init("https://my.115.com/?ct=ajax&ac=nav") 11 | return s 12 | } 13 | -------------------------------------------------------------------------------- /lowlevel/api/video.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "github.com/deadblue/elevengo/internal/protocol" 5 | "github.com/deadblue/elevengo/lowlevel/types" 6 | ) 7 | 8 | type VideoPlayWebSpec struct { 9 | _JsonApiSpec[types.VideoPlayResult, protocol.VideoPlayWebResp] 10 | } 11 | 12 | func (s *VideoPlayWebSpec) Init(pickcode string) *VideoPlayWebSpec { 13 | s._JsonApiSpec.Init("https://webapi.115.com/files/video") 14 | s.query.Set("pickcode", pickcode) 15 | return s 16 | } 17 | 18 | // type VideoPlayPcSpec struct { 19 | // _M115ApiSpec[types.VideoPlayResult] 20 | // } 21 | 22 | // func (s *VideoPlayPcSpec) Init(pickcode string, common *types.CommonParams) *VideoPlayPcSpec { 23 | // s._M115ApiSpec.Init("https://proapi.115.com/pc/video/play") 24 | // s.params.Set("format", "app"). 25 | // Set("appversion", common.AppVer). 26 | // Set("user_id", common.UserId). 27 | // Set("definition_filter", "1"). 28 | // Set("pickcode", pickcode) 29 | // return s 30 | // } 31 | 32 | type VideoSubtitleSpec struct { 33 | _StandardApiSpec[types.VideoSubtitleResult] 34 | } 35 | 36 | func (s *VideoSubtitleSpec) Init(pickcode string) *VideoSubtitleSpec { 37 | s._StandardApiSpec.Init("https://webapi.115.com/movies/subtitle") 38 | s.query.Set("pickcode", pickcode) 39 | return s 40 | } 41 | -------------------------------------------------------------------------------- /lowlevel/client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "context" 5 | ) 6 | 7 | // Client is the low-level client which executes ApiSpec. 8 | type Client interface { 9 | 10 | // GetUserAgent returns current "User-Agent" value. 11 | GetUserAgent() string 12 | 13 | // ExportCookies exports cookies for specific URL. 14 | ExportCookies(url string) map[string]string 15 | 16 | // CallApi calls an API. 17 | CallApi(spec ApiSpec, context context.Context) error 18 | 19 | // Get performs an HTTP GET request. 20 | Get( 21 | url string, headers map[string]string, context context.Context, 22 | ) (body Body, err error) 23 | 24 | // Post performs an HTTP POST request. 25 | Post( 26 | url string, payload Payload, headers map[string]string, context context.Context, 27 | ) (body Body, err error) 28 | } 29 | -------------------------------------------------------------------------------- /lowlevel/client/types.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "io" 4 | 5 | type ( 6 | // Payload describes the request body. 7 | Payload interface { 8 | io.Reader 9 | 10 | // ContentType returns the MIME type of payload. 11 | ContentType() string 12 | 13 | // ContentLength returns the size in bytes of payload. 14 | ContentLength() int64 15 | } 16 | 17 | // ApiSpec describes the specification of an 115 API. 18 | ApiSpec interface { 19 | 20 | // IsCrypto indicates whether the API request uses EC-crypto. 21 | IsCrypto() bool 22 | 23 | // SetCryptoKey adds crypto key in parameters. 24 | SetCryptoKey(key string) 25 | 26 | // Url returns the request URL of API. 27 | Url() string 28 | 29 | // Payload returns the request body of API. 30 | Payload() Payload 31 | 32 | // Parse parses the response body. 33 | Parse(r io.Reader) (err error) 34 | } 35 | 36 | Body interface { 37 | io.ReadCloser 38 | 39 | // Size returns body size or -1 when unknown. 40 | Size() int64 41 | } 42 | ) 43 | -------------------------------------------------------------------------------- /lowlevel/doc.go: -------------------------------------------------------------------------------- 1 | // Package lowlevel exposes the low-level API client and specs that are used 2 | // by Agent. 3 | package lowlevel 4 | -------------------------------------------------------------------------------- /lowlevel/errors/errors.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrNotLogin = errors.New("user not login") 7 | 8 | ErrCaptchaRequired = errors.New("please resolve captcha") 9 | 10 | ErrOfflineInvalidLink = errors.New("invalid download link") 11 | 12 | ErrIPAbnormal = errors.New("ip abnormal") 13 | ErrPasswordIncorrect = errors.New("password incorrect") 14 | ErrLoginTwoStepVerify = errors.New("requires two-step verification") 15 | ErrAccountNotBindMobile = errors.New("account not binds mobile") 16 | ErrCredentialInvalid = errors.New("credential invalid") 17 | ErrSessionExited = errors.New("session exited") 18 | 19 | ErrQrcodeExpired = errors.New("qrcode expired") 20 | ErrGetFailed = errors.New("get failed") 21 | 22 | // ErrUnexpected is the fall-back error whose code is not handled. 23 | ErrUnexpected = errors.New("unexpected error") 24 | 25 | // ErrExist means an item which you want to create is already existed. 26 | ErrExist = errors.New("target already exists") 27 | // ErrNotExist means an item which you find is not existed. 28 | ErrNotExist = errors.New("target does not exist") 29 | 30 | ErrInvalidOperation = errors.New("invalid operation") 31 | 32 | ErrInvalidParameters = errors.New("invalid parameters") 33 | 34 | // ErrReachEnd means there are no more item. 35 | // ErrReachEnd = errors.New("reach the end") 36 | 37 | ErrUploadDisabled = errors.New("upload function is disabled") 38 | 39 | ErrUploadNothing = errors.New("nothing ot upload") 40 | 41 | ErrUploadTooLarge = errors.New("upload reach the limit") 42 | 43 | ErrInitUploadUnknowStatus = errors.New("unknown status from initupload") 44 | 45 | ErrImportDirectory = errors.New("can not import directory") 46 | 47 | ErrDownloadEmpty = errors.New("can not get download URL") 48 | 49 | ErrDownloadDirectory = errors.New("can not download directory") 50 | 51 | ErrVideoNotReady = errors.New("video is not ready") 52 | 53 | ErrEmptyList = errors.New("list is empty") 54 | ) 55 | -------------------------------------------------------------------------------- /lowlevel/errors/fault.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ErrInvalidResp = errors.New("invalid resp") 6 | -------------------------------------------------------------------------------- /lowlevel/errors/file.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | const ( 4 | CodeFileOrderNotSupported = 20130827 5 | ) 6 | 7 | type FileOrderInvalidError struct { 8 | Order string 9 | Asc int 10 | } 11 | 12 | func (e *FileOrderInvalidError) Error() string { 13 | return "invalid file order" 14 | } 15 | -------------------------------------------------------------------------------- /lowlevel/errors/get.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "fmt" 4 | 5 | var errorsMap = map[int]error{ 6 | // Normal errors 7 | 99: ErrNotLogin, 8 | 911: ErrCaptchaRequired, 9 | 990001: ErrNotLogin, 10 | // Offline errors 11 | CodeOfflineIllegalLink: ErrOfflineInvalidLink, 12 | // File errors 13 | 20004: ErrExist, 14 | 20022: ErrInvalidOperation, 15 | // Label errors 16 | 21003: ErrExist, 17 | // Download errors 18 | 50003: ErrNotExist, 19 | // Common errors 20 | 990002: ErrInvalidParameters, 21 | // Login errors 22 | 40101004: ErrIPAbnormal, 23 | 40101009: ErrPasswordIncorrect, 24 | 40101010: ErrLoginTwoStepVerify, 25 | 40101030: ErrAccountNotBindMobile, 26 | 40101032: ErrCredentialInvalid, 27 | 40101037: ErrSessionExited, 28 | // QRCode errors 29 | 40199002: ErrQrcodeExpired, 30 | 50199004: ErrGetFailed, 31 | 32 | // Whitelist errors 33 | CodeOfflineTaskExists: nil, 34 | } 35 | 36 | type ApiError struct { 37 | Code int 38 | Message string 39 | } 40 | 41 | func (e *ApiError) Error() string { 42 | return fmt.Sprintf("(%d)%s", e.Code, e.Message) 43 | } 44 | 45 | func Get(code int, message string) error { 46 | if err, found := errorsMap[code]; found { 47 | return err 48 | } 49 | return &ApiError{ 50 | Code: code, 51 | Message: message, 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /lowlevel/errors/media.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | import "errors" 4 | 5 | var ( 6 | ErrUnsupportedPlatform = errors.New("unsupported platform") 7 | ) 8 | -------------------------------------------------------------------------------- /lowlevel/errors/offline.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | const ( 4 | CodeOfflineIllegalLink = 10004 5 | CodeOfflineTaskExists = 10008 6 | ) 7 | -------------------------------------------------------------------------------- /lowlevel/errors/share.go: -------------------------------------------------------------------------------- 1 | package errors 2 | 3 | const ( 4 | ShareClosed = 4100000 5 | 6 | ShareReceiveCodeIncorrect = 4100008 7 | 8 | ShareCanceled1 = 4100009 9 | ShareCanceled2 = 4100010 10 | ShareCanceled3 = 4100018 11 | ShareCanceled4 = 4100033 12 | 13 | ShareRequireReceiveCode = 4100012 14 | ) 15 | -------------------------------------------------------------------------------- /lowlevel/types/agent.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/deadblue/elevengo/internal/crypto/hash" 7 | ) 8 | 9 | // Common parameters for several APIs 10 | type CommonParams struct { 11 | // App version 12 | AppVer string 13 | // User ID 14 | UserId string 15 | // MD5 hash of user ID 16 | UserHash string 17 | // User key for uploading 18 | UserKey string 19 | } 20 | 21 | func (c *CommonParams) SetUserInfo(userId int, userKey string) { 22 | c.UserId = strconv.Itoa(userId) 23 | c.UserHash = hash.Md5Hex(c.UserId) 24 | c.UserKey = userKey 25 | } 26 | -------------------------------------------------------------------------------- /lowlevel/types/app.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type AppVersionInfo struct { 4 | AppOs int `json:"app_os"` 5 | CreatedTime int64 `json:"created_time"` 6 | VersionCode string `json:"version_code"` 7 | VersionUrl string `json:"version_url"` 8 | } 9 | 10 | type AppVersionResult map[string]*AppVersionInfo 11 | -------------------------------------------------------------------------------- /lowlevel/types/download.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "encoding/json" 4 | 5 | type _DownloadUrlProto struct { 6 | Url string `json:"url"` 7 | Client int `json:"client"` 8 | OssId string `json:"oss_id"` 9 | } 10 | 11 | type DownloadUrl struct { 12 | Url string 13 | } 14 | 15 | func (u *DownloadUrl) UnmarshalJSON(b []byte) (err error) { 16 | if len(b) > 0 && b[0] == '{' { 17 | proto := &_DownloadUrlProto{} 18 | if err = json.Unmarshal(b, proto); err == nil { 19 | u.Url = proto.Url 20 | } 21 | } 22 | return 23 | } 24 | 25 | type DownloadInfo struct { 26 | FileName string `json:"file_name"` 27 | FileSize json.Number `json:"file_size"` 28 | PickCode string `json:"pick_code"` 29 | Url DownloadUrl `json:"url"` 30 | } 31 | 32 | type DownloadResult map[string]*DownloadInfo 33 | -------------------------------------------------------------------------------- /lowlevel/types/file.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/deadblue/elevengo/internal/util" 4 | 5 | type FileInfo struct { 6 | AreaId util.IntNumber `json:"aid"` 7 | CategoryId string `json:"cid"` 8 | FileId string `json:"fid"` 9 | ParentId string `json:"pid"` 10 | 11 | Name string `json:"n"` 12 | Type string `json:"ico"` 13 | Size util.IntNumber `json:"s"` 14 | Sha1 string `json:"sha"` 15 | PickCode string `json:"pc"` 16 | 17 | IsStar util.Boolean `json:"m"` 18 | Labels []*LabelInfo `json:"fl"` 19 | 20 | CreatedTime string `json:"tp"` 21 | UpdatedTime string `json:"te"` 22 | ModifiedTime string `json:"t"` 23 | 24 | // MediaDuration describes duration in seconds for audio/video. 25 | MediaDuration float64 `json:"play_long"` 26 | 27 | // Special fields for video 28 | VideoFlag int `json:"iv"` 29 | VideoDefinition int `json:"vdi"` 30 | } 31 | 32 | type FileListResult struct { 33 | DirId string 34 | Offset int 35 | 36 | Count int 37 | Files []*FileInfo 38 | 39 | // Order settings 40 | Order string 41 | Asc int 42 | } 43 | 44 | type FileGetResult []*FileInfo 45 | -------------------------------------------------------------------------------- /lowlevel/types/image.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ImageGetResult struct { 4 | FileName string `json:"file_name"` 5 | FileSha1 string `json:"file_sha1"` 6 | Pickcode string `json:"pick_code"` 7 | 8 | SourceUrl string `json:"source_url"` 9 | OriginUrl string `json:"origin_url"` 10 | ViewUrl string `json:"url"` 11 | ThumbUrls []string `json:"all_url"` 12 | } 13 | -------------------------------------------------------------------------------- /lowlevel/types/index.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type SizeInfo struct { 4 | Size float64 `json:"size"` 5 | SizeFormat string `json:"size_format"` 6 | } 7 | 8 | type LoginInfo struct { 9 | IsCurrent int `json:"is_current"` 10 | LoginTime int64 `json:"utime"` 11 | AppFlag string `json:"ssoent"` 12 | AppName string `json:"name"` 13 | Ip string `json:"ip"` 14 | City string `json:"city"` 15 | } 16 | 17 | type IndexInfoResult struct { 18 | SpaceInfo struct { 19 | Total SizeInfo `json:"all_total"` 20 | Remain SizeInfo `json:"all_remain"` 21 | Used SizeInfo `json:"all_use"` 22 | } `json:"space_info"` 23 | LoginInfos struct { 24 | List []*LoginInfo 25 | } `json:"login_devices_info"` 26 | } 27 | -------------------------------------------------------------------------------- /lowlevel/types/label.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/deadblue/elevengo/internal/util" 4 | 5 | type LabelInfo struct { 6 | Id string `json:"id"` 7 | Name string `json:"name"` 8 | Color string `json:"color"` 9 | Sort util.IntNumber `json:"sort"` 10 | CreateTime int64 `json:"create_time"` 11 | UpdateTime int64 `json:"update_time"` 12 | } 13 | 14 | type LabelListResult struct { 15 | Total int `json:"total"` 16 | List []*LabelInfo `json:"list"` 17 | Sort string `json:"sort"` 18 | Order string `json:"order"` 19 | } 20 | 21 | type LabelCreateResult []*LabelInfo 22 | -------------------------------------------------------------------------------- /lowlevel/types/offline.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/deadblue/elevengo/lowlevel/errors" 7 | ) 8 | 9 | type TaskInfo struct { 10 | InfoHash string `json:"info_hash"` 11 | Name string `json:"name"` 12 | Size int64 `json:"size"` 13 | Url string `json:"url"` 14 | AddTime int64 `json:"add_time"` 15 | 16 | Status int `json:"status"` 17 | Percent float64 `json:"percentDone"` 18 | UpdateTime int64 `json:"last_update"` 19 | 20 | FileId string `json:"file_id"` 21 | DirId string `json:"wp_path_id"` 22 | } 23 | 24 | type OfflineListResult struct { 25 | PageIndex int 26 | PageCount int 27 | PageSize int 28 | 29 | QuotaTotal int 30 | QuotaRemain int 31 | 32 | TaskCount int 33 | Tasks []*TaskInfo 34 | } 35 | 36 | type _OfflineAddResult struct { 37 | State bool `json:"state"` 38 | ErrNum int `json:"errno"` 39 | ErrCode int `json:"errcode"` 40 | ErrType string `json:"errtype"` 41 | 42 | InfoHash string `json:"info_hash"` 43 | Name string `json:"name"` 44 | Url string `json:"url"` 45 | } 46 | 47 | type _OfflineAddUrlsProto struct { 48 | State bool `json:"state"` 49 | ErrNum int `json:"errno"` 50 | ErrCode int `json:"errcode"` 51 | 52 | Result []*_OfflineAddResult `json:"result"` 53 | } 54 | 55 | type OfflineAddUrlsResult []*TaskInfo 56 | 57 | func (r *OfflineAddUrlsResult) UnmarshalResult(data []byte) (err error) { 58 | proto := &_OfflineAddUrlsProto{} 59 | if err = json.Unmarshal(data, proto); err != nil { 60 | return 61 | } 62 | tasks := make([]*TaskInfo, len(proto.Result)) 63 | for i, r := range proto.Result { 64 | if r.State || r.ErrCode == errors.CodeOfflineTaskExists { 65 | tasks[i] = &TaskInfo{} 66 | tasks[i].InfoHash = r.InfoHash 67 | tasks[i].Name = r.Name 68 | tasks[i].Url = r.Url 69 | } else { 70 | tasks[i] = nil 71 | } 72 | } 73 | *r = tasks 74 | return 75 | } 76 | -------------------------------------------------------------------------------- /lowlevel/types/qrcode.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type QrcodeTokenResult struct { 4 | Uid string `json:"uid"` 5 | Time int64 `json:"time"` 6 | Sign string `json:"sign"` 7 | } 8 | 9 | type QrcodeStatusResult struct { 10 | Status int `json:"status,omitempty"` 11 | Message string `json:"msg,omitempty"` 12 | Version string `json:"version,omitempty"` 13 | } 14 | 15 | type QrcodeLoginResult struct { 16 | Cookie struct { 17 | UID string `json:"UID"` 18 | CID string `json:"CID"` 19 | KID string `json:"KID"` 20 | SEID string `json:"SEID"` 21 | } `json:"cookie"` 22 | UserId int `json:"user_id"` 23 | UserName string `json:"user_name"` 24 | } 25 | -------------------------------------------------------------------------------- /lowlevel/types/recycle.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/deadblue/elevengo/internal/util" 4 | 5 | type RecycleBinItem struct { 6 | FileId string `json:"id"` 7 | FileName string `json:"file_name"` 8 | FileSize util.IntNumber `json:"file_size"` 9 | ParentId string `json:"cid"` 10 | ParentName string `json:"parent_name"` 11 | DeleteTime util.IntNumber `json:"dtime"` 12 | } 13 | 14 | type RecycleBinListResult struct { 15 | Count int 16 | Item []*RecycleBinItem 17 | } 18 | -------------------------------------------------------------------------------- /lowlevel/types/share.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import "github.com/deadblue/elevengo/internal/util" 4 | 5 | const ( 6 | ShareStateAuditing = 0 7 | ShareStateAccepted = 1 8 | ShareStateRejected1 = 2 9 | ShareStateRejected2 = 3 10 | ShareStateCanceled = 4 11 | ShareStateDeleted = 5 12 | ShareStateRejected3 = 6 13 | ShareStateExpired = 7 14 | ShareStateGenerating = 8 15 | ShareStateFailed = 9 16 | ShareStateRejected4 = 11 17 | ) 18 | 19 | type ShareInfo struct { 20 | ShareCode string `json:"share_code"` 21 | 22 | ShareState util.IntNumber `json:"share_state"` 23 | ShareTitle string `json:"share_title"` 24 | ShareUrl string `json:"share_url"` 25 | ShareDuration util.IntNumber `json:"share_ex_time"` 26 | ReceiveCode string `json:"receive_code"` 27 | 28 | ReceiveCount util.IntNumber `json:"receive_count"` 29 | 30 | FileCount int `json:"file_count"` 31 | FolderCount int `json:"folder_count"` 32 | TotalSize util.IntNumber `json:"total_size"` 33 | } 34 | 35 | type ShareListResult struct { 36 | Count int 37 | Items []*ShareInfo 38 | } 39 | 40 | type ShareFileInfo struct { 41 | FileId string 42 | IsDir bool 43 | Name string 44 | Size int64 45 | Sha1 string 46 | CreateTime int64 47 | IsVideo bool 48 | VideoDefinition int 49 | MediaDuration int 50 | } 51 | 52 | type ShareSnapResult struct { 53 | SnapId string 54 | UserId int 55 | ShareTitle string 56 | ShareState int 57 | ReceiveCount int 58 | CreateTime int64 59 | ExpireTime int64 60 | 61 | TotalSize int64 62 | FileCount int 63 | Files []*ShareFileInfo 64 | } 65 | -------------------------------------------------------------------------------- /lowlevel/types/shortcut.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type ShortcutInfo struct { 4 | FileId string `json:"file_id"` 5 | FileName string `json:"file_name"` 6 | Sort string `json:"sort"` 7 | } 8 | 9 | type ShortcutListResult struct { 10 | List []*ShortcutInfo `json:"list"` 11 | } 12 | -------------------------------------------------------------------------------- /lowlevel/types/upload.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | ) 8 | 9 | type UploadInfoResult struct { 10 | UserId int 11 | UserKey string 12 | } 13 | 14 | type UploadInitResult struct { 15 | Exists bool 16 | // Upload parameters 17 | Oss struct { 18 | Bucket string 19 | Object string 20 | Callback string 21 | CallbackVar string 22 | } 23 | // Check parameters 24 | SignKey string 25 | SignCheck string 26 | // Pickcode is available when rapid-uploaded 27 | PickCode string 28 | } 29 | 30 | type UploadTokenResult struct { 31 | AccessKeyId string 32 | AccessKeySecret string 33 | SecurityToken string 34 | Expiration time.Time 35 | } 36 | 37 | type UploadSampleInitResult struct { 38 | Host string 39 | Object string 40 | Callback string 41 | AccessKeyId string 42 | Policy string 43 | Signature string 44 | } 45 | 46 | type UploadSampleResult struct { 47 | AreaId util.IntNumber `json:"aid"` 48 | CategoryId string `json:"cid"` 49 | FileId string `json:"file_id"` 50 | FileName string `json:"file_name"` 51 | FileSize util.IntNumber `json:"file_size"` 52 | FileSha1 string `json:"sha1"` 53 | PickCode string `json:"pick_code"` 54 | CreateTime util.IntNumber `json:"file_ptime"` 55 | } 56 | -------------------------------------------------------------------------------- /lowlevel/types/user.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | type UserInfoResult struct { 4 | UserId int `json:"user_id"` 5 | UserName string `json:"user_name"` 6 | AvatarUrl string `json:"face"` 7 | IsVip int `json:"vip"` 8 | } 9 | -------------------------------------------------------------------------------- /lowlevel/types/video.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | ) 8 | 9 | type VideoInfo struct { 10 | Width int 11 | Height int 12 | PlayUrl string 13 | } 14 | 15 | type VideoPlayResult struct { 16 | IsReady bool 17 | FileId string 18 | FileName string 19 | FileSize int64 20 | VideoDuration float64 21 | Videos []*VideoInfo 22 | } 23 | 24 | type _VideoUrl struct { 25 | Title string `json:"title"` 26 | Definition int `json:"definition"` 27 | Width util.IntNumber `json:"width"` 28 | Height util.IntNumber `json:"height"` 29 | Url string `json:"url"` 30 | } 31 | 32 | type _VideoPlayPcProto struct { 33 | FileId string `json:"file_id"` 34 | ParentId string `json:"parent_id"` 35 | FileName string `json:"file_name"` 36 | FileSize util.IntNumber `json:"file_size"` 37 | FileSha1 string `json:"file_sha1"` 38 | PickCode string `json:"pick_code"` 39 | FileStatus int `json:"file_status"` 40 | VideoDuration util.FloatNumner `json:"play_long"` 41 | VideoUrls []*_VideoUrl `json:"video_url"` 42 | } 43 | 44 | func (r *VideoPlayResult) UnmarshalResult(data []byte) (err error) { 45 | proto := &_VideoPlayPcProto{} 46 | if err = json.Unmarshal(data, proto); err != nil { 47 | return 48 | } 49 | r.IsReady = proto.FileStatus == 1 50 | r.FileId = proto.FileId 51 | r.FileName = proto.FileName 52 | r.FileSize = proto.FileSize.Int64() 53 | r.VideoDuration = proto.VideoDuration.Float64() 54 | r.Videos = make([]*VideoInfo, len(proto.VideoUrls)) 55 | for index, vu := range proto.VideoUrls { 56 | r.Videos[index] = &VideoInfo{ 57 | Width: vu.Width.Int(), 58 | Height: vu.Height.Int(), 59 | PlayUrl: vu.Url, 60 | } 61 | } 62 | return nil 63 | } 64 | 65 | type _VideoSubtitleProto struct { 66 | SubtitleId string `json:"sid"` 67 | Language string `json:"language"` 68 | 69 | Title string `json:"title"` 70 | Type string `json:"type"` 71 | Url string `json:"url"` 72 | 73 | SyncTime int `json:"sync_time"` 74 | 75 | IsCaptionMap int `json:"is_caption_map"` 76 | CaptionMapId string `json:"caption_map_id"` 77 | 78 | FileId string `json:"file_id"` 79 | FileName string `json:"file_name"` 80 | PickCode string `json:"pick_code"` 81 | Sha1 string `json:"sha1"` 82 | } 83 | 84 | type VideoSubtitleInfo struct { 85 | Language string 86 | Title string 87 | Type string 88 | Url string 89 | } 90 | 91 | func (i *VideoSubtitleInfo) UnmarshalJSON(data []byte) (err error) { 92 | if len(data) > 0 && data[0] == '{' { 93 | proto := &_VideoSubtitleProto{} 94 | if err = json.Unmarshal(data, proto); err == nil { 95 | i.Language = proto.Language 96 | i.Title = proto.Title 97 | i.Type = proto.Type 98 | i.Url = proto.Url 99 | } 100 | } 101 | return 102 | } 103 | 104 | type VideoSubtitleResult struct { 105 | AutoLoad VideoSubtitleInfo `json:"autoload"` 106 | List []*VideoSubtitleInfo `json:"list"` 107 | } 108 | -------------------------------------------------------------------------------- /lowlevel/types/void.go: -------------------------------------------------------------------------------- 1 | package types 2 | 3 | // VoidResult describes a void result. 4 | type VoidResult struct{} 5 | -------------------------------------------------------------------------------- /media.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deadblue/elevengo/internal/util" 7 | "github.com/deadblue/elevengo/lowlevel/api" 8 | "github.com/deadblue/elevengo/lowlevel/errors" 9 | ) 10 | 11 | // VideoDefinition values from 115. 12 | type VideoDefinition int 13 | 14 | const ( 15 | // Standard Definition, aka. 480P. 16 | VideoDefinitionSD VideoDefinition = 1 17 | // High Definition, aka. 720P. 18 | VideoDefinitionHD VideoDefinition = 2 19 | // Full-HD, aka. 1080P. 20 | VideoDefinitionFHD VideoDefinition = 3 21 | // Another 1080P, what the fuck? 22 | VideoDefinition1080P VideoDefinition = 4 23 | // 4K Definition, aka. Ultra-HD. 24 | VideoDefinition4K VideoDefinition = 5 25 | // The fallback definition, usually for non-standard resolution. 26 | VideoDefinitionOrigin VideoDefinition = 100 27 | ) 28 | 29 | // VideoTicket contains all required arguments to play a cloud video. 30 | type VideoTicket struct { 31 | // Play URL, it is normally a m3u8 URL. 32 | Url string 33 | // Request headers which SHOULD be sent with play URL. 34 | Headers map[string]string 35 | // File name. 36 | FileName string 37 | // File size. 38 | FileSize int64 39 | // Video duration in seconds. 40 | Duration float64 41 | // Video width. 42 | Width int 43 | // Video height. 44 | Height int 45 | } 46 | 47 | // VideoCreateTicket creates a PlayTicket to play the cloud video. 48 | func (a *Agent) VideoCreateTicket(pickcode string, ticket *VideoTicket) (err error) { 49 | if !a.isWeb { 50 | return errors.ErrUnsupportedPlatform 51 | } 52 | spec := (&api.VideoPlayWebSpec{}).Init(pickcode) 53 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 54 | return 55 | } 56 | if !spec.Result.IsReady { 57 | return errors.ErrVideoNotReady 58 | } 59 | ticket.FileName = spec.Result.FileName 60 | ticket.FileSize = spec.Result.FileSize 61 | ticket.Duration = spec.Result.VideoDuration 62 | // Select the video with best definition 63 | for _, video := range spec.Result.Videos { 64 | if video.Width > ticket.Width { 65 | ticket.Width = video.Width 66 | ticket.Height = video.Height 67 | ticket.Url = video.PlayUrl 68 | } 69 | } 70 | ticket.Headers = map[string]string{ 71 | "User-Agent": a.llc.GetUserAgent(), 72 | "Cookie": util.MarshalCookies(a.llc.ExportCookies(ticket.Url)), 73 | } 74 | return 75 | } 76 | 77 | // ImageGetUrl gets an accessible URL of an image file by its pickcode. 78 | func (a *Agent) ImageGetUrl(pickcode string) (imageUrl string, err error) { 79 | spec := (&api.ImageGetSpec{}).Init(pickcode) 80 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 81 | return 82 | } 83 | // The origin URL can be access without cookie. 84 | imageUrl = spec.Result.OriginUrl 85 | return 86 | } 87 | -------------------------------------------------------------------------------- /offline.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "iter" 6 | 7 | "github.com/deadblue/elevengo/internal/util" 8 | "github.com/deadblue/elevengo/lowlevel/api" 9 | "github.com/deadblue/elevengo/lowlevel/client" 10 | "github.com/deadblue/elevengo/lowlevel/types" 11 | "github.com/deadblue/elevengo/option" 12 | ) 13 | 14 | type OfflineClearFlag int 15 | 16 | const ( 17 | OfflineClearDone OfflineClearFlag = iota 18 | OfflineClearAll 19 | OfflineClearFailed 20 | OfflineClearRunning 21 | OfflineClearDoneAndDelete 22 | OfflineClearAllAndDelete 23 | 24 | offlineClearFlagMin = OfflineClearDone 25 | offlineClearFlagMax = OfflineClearAllAndDelete 26 | ) 27 | 28 | // OfflineTask describe an offline downloading task. 29 | type OfflineTask struct { 30 | InfoHash string 31 | Name string 32 | Size int64 33 | Status int 34 | Percent float64 35 | Url string 36 | FileId string 37 | } 38 | 39 | func (t *OfflineTask) IsRunning() bool { 40 | return t.Status == 1 41 | } 42 | 43 | func (t *OfflineTask) IsDone() bool { 44 | return t.Status == 2 45 | } 46 | 47 | func (t *OfflineTask) IsFailed() bool { 48 | return t.Status == -1 49 | } 50 | 51 | func (t *OfflineTask) from(ti *types.TaskInfo) *OfflineTask { 52 | t.InfoHash = ti.InfoHash 53 | t.Name = ti.Name 54 | t.Size = ti.Size 55 | t.Status = ti.Status 56 | t.Percent = ti.Percent 57 | t.Url = ti.Url 58 | t.FileId = ti.FileId 59 | return t 60 | } 61 | 62 | type offlineIterator struct { 63 | llc client.Client 64 | page int 65 | result *types.OfflineListResult 66 | } 67 | 68 | func (i *offlineIterator) update() (err error) { 69 | if i.result != nil && i.page > i.result.PageCount { 70 | return errNoMoreItems 71 | } 72 | spec := (&api.OfflineListSpec{}).Init(i.page) 73 | if err = i.llc.CallApi(spec, context.Background()); err == nil { 74 | i.result = &spec.Result 75 | i.page += 1 76 | } 77 | return 78 | } 79 | 80 | func (i *offlineIterator) Count() int { 81 | if i.result == nil { 82 | return 0 83 | } 84 | return i.result.TaskCount 85 | } 86 | 87 | func (i *offlineIterator) Items() iter.Seq2[int, *OfflineTask] { 88 | return func(yield func(int, *OfflineTask) bool) { 89 | for index := 0; ; { 90 | for _, ti := range i.result.Tasks { 91 | if stop := !yield(index, (&OfflineTask{}).from(ti)); stop { 92 | return 93 | } 94 | index += 1 95 | } 96 | if err := i.update(); err != nil { 97 | break 98 | } 99 | } 100 | } 101 | } 102 | 103 | // OfflineIterate returns an iterator to access all offline tasks. 104 | func (a *Agent) OfflineIterate() (it Iterator[OfflineTask], err error) { 105 | oi := &offlineIterator{ 106 | llc: a.llc, 107 | page: 1, 108 | } 109 | if err = oi.update(); err == nil { 110 | it = oi 111 | } 112 | return 113 | } 114 | 115 | // OfflineDelete deletes tasks. 116 | func (a *Agent) OfflineDelete(hashes []string, options ...*option.OfflineDeleteOptions) (err error) { 117 | if len(hashes) == 0 { 118 | return 119 | } 120 | // Apply options 121 | deleteFiles := false 122 | if opts := util.NotNull(options...); opts != nil { 123 | deleteFiles = opts.DeleteFiles 124 | } 125 | // Call API 126 | spec := (&api.OfflineDeleteSpec{}).Init(hashes, deleteFiles) 127 | return a.llc.CallApi(spec, context.Background()) 128 | } 129 | 130 | // OfflineClear clears tasks which is in specific status. 131 | func (a *Agent) OfflineClear(flag OfflineClearFlag) (err error) { 132 | if flag < offlineClearFlagMin || flag > offlineClearFlagMax { 133 | flag = OfflineClearDone 134 | } 135 | spec := (&api.OfflineClearSpec{}).Init(int(flag)) 136 | return a.llc.CallApi(spec, context.Background()) 137 | } 138 | 139 | // OfflineAddUrl adds offline tasks by download URLs. 140 | // It returns an info hash list related to the given urls, the info hash will 141 | // be empty if the related URL is invalid. 142 | // 143 | // You can use options to change the download directory: 144 | // 145 | // agent := Default() 146 | // agent.CredentialImport(&Credential{UID: "", CID: "", SEID: ""}) 147 | // hashes, err := agent.OfflineAddUrl([]string{ 148 | // "https://foo.bar/file.zip", 149 | // "magent:?xt=urn:btih:111222", 150 | // "ed2k://|file|name|size|md4|", 151 | // }, option.OfflineSaveDownloadedFileTo("dirId")) 152 | func (a *Agent) OfflineAddUrl(urls []string, options ...*option.OfflineAddOptions) (hashes []string, err error) { 153 | // Prepare results buffer 154 | if urlCount := len(urls); urlCount == 0 { 155 | return 156 | } else { 157 | hashes = make([]string, urlCount) 158 | } 159 | // Apply options 160 | saveDirId := "" 161 | if opts := util.NotNull(options...); opts != nil { 162 | saveDirId = opts.SaveDirId 163 | } 164 | // Call API 165 | spec := (&api.OfflineAddUrlsSpec{}).Init(urls, saveDirId, &a.common) 166 | if err = a.llc.CallApi(spec, context.Background()); err == nil { 167 | for i, task := range spec.Result { 168 | if task != nil { 169 | hashes[i] = task.InfoHash 170 | } 171 | } 172 | } 173 | return 174 | } 175 | -------------------------------------------------------------------------------- /option/agent.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | import "github.com/deadblue/elevengo/plugin" 4 | 5 | type AgentOptions struct { 6 | // Underlying HTTP client which is used to perform HTTP request. 7 | HttpClient plugin.HttpClient 8 | 9 | // Minimum delay in milliseconds after last API calling. 10 | CooldownMinMs uint 11 | 12 | // Maximum delay in milliseconds after last API calling. 13 | CooldownMaxMs uint 14 | 15 | // Custom user-agent. 16 | Name string 17 | 18 | // Custom app version. 19 | Version string 20 | } 21 | 22 | func (o *AgentOptions) WithHttpClient(hc plugin.HttpClient) *AgentOptions { 23 | o.HttpClient = hc 24 | return o 25 | } 26 | 27 | func (o *AgentOptions) WithCooldown(minMs, maxMs uint) *AgentOptions { 28 | o.CooldownMinMs = minMs 29 | o.CooldownMaxMs = maxMs 30 | return o 31 | } 32 | 33 | func (o *AgentOptions) WithName(name string) *AgentOptions { 34 | o.Name = name 35 | return o 36 | } 37 | func (o *AgentOptions) WithVersion(version string) *AgentOptions { 38 | o.Version = version 39 | return o 40 | } 41 | 42 | func Agent() *AgentOptions { 43 | return &AgentOptions{} 44 | } 45 | -------------------------------------------------------------------------------- /option/file.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | type FileListOptions struct { 4 | /*Predefined file type: 5 | - 0: All 6 | - 1: Document 7 | - 2: Image 8 | - 3: Audio 9 | - 4: Video 10 | - 5: Archive 11 | - 6: Sofrwore 12 | */ 13 | Type int 14 | // File extension with leading-dot, e.g.: mkv 15 | ExtName string 16 | } 17 | 18 | func (o *FileListOptions) ShowAll() *FileListOptions { 19 | o.Type = 0 20 | o.ExtName = "" 21 | return o 22 | } 23 | 24 | func (o *FileListOptions) OnlyDocument() *FileListOptions { 25 | o.Type = 1 26 | o.ExtName = "" 27 | return o 28 | } 29 | 30 | func (o *FileListOptions) OnlyImage() *FileListOptions { 31 | o.Type = 2 32 | o.ExtName = "" 33 | return o 34 | } 35 | 36 | func (o *FileListOptions) OnlyAudio() *FileListOptions { 37 | o.Type = 3 38 | o.ExtName = "" 39 | return o 40 | } 41 | 42 | func (o *FileListOptions) OnlyVideo() *FileListOptions { 43 | o.Type = 4 44 | o.ExtName = "" 45 | return o 46 | } 47 | 48 | func (o *FileListOptions) OnlyArchive() *FileListOptions { 49 | o.Type = 5 50 | o.ExtName = "" 51 | return o 52 | } 53 | 54 | func (o *FileListOptions) OnlySoftware() *FileListOptions { 55 | o.Type = 6 56 | o.ExtName = "" 57 | return o 58 | } 59 | 60 | func (o *FileListOptions) OnlyExtension(extName string) *FileListOptions { 61 | o.Type = -1 62 | o.ExtName = extName 63 | return o 64 | } 65 | 66 | func FileList() *FileListOptions { 67 | return (&FileListOptions{}).ShowAll() 68 | } 69 | -------------------------------------------------------------------------------- /option/offline.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | type OfflineAddOptions struct { 4 | SaveDirId string 5 | } 6 | 7 | func (o *OfflineAddOptions) WithSaveDirId(dirId string) *OfflineAddOptions { 8 | o.SaveDirId = dirId 9 | return o 10 | } 11 | 12 | func OfflineAdd() *OfflineAddOptions { 13 | return &OfflineAddOptions{} 14 | } 15 | 16 | type OfflineDeleteOptions struct { 17 | DeleteFiles bool 18 | } 19 | 20 | func (o *OfflineDeleteOptions) DeleteDownloadedFiles() *OfflineDeleteOptions { 21 | o.DeleteFiles = true 22 | return o 23 | } 24 | 25 | func OfflineDelete() *OfflineDeleteOptions { 26 | return &OfflineDeleteOptions{} 27 | } 28 | -------------------------------------------------------------------------------- /option/qrcode.go: -------------------------------------------------------------------------------- 1 | package option 2 | 3 | type QrcodeOptions struct { 4 | // App Type to login 5 | App string 6 | } 7 | 8 | func (o *QrcodeOptions) LoginWeb() *QrcodeOptions { 9 | o.App = "web" 10 | return o 11 | } 12 | 13 | func (o *QrcodeOptions) LoginAndroid() *QrcodeOptions { 14 | o.App = "android" 15 | return o 16 | } 17 | 18 | func (o *QrcodeOptions) LoginIos() *QrcodeOptions { 19 | o.App = "ios" 20 | return o 21 | } 22 | 23 | func (o *QrcodeOptions) LoginTv() *QrcodeOptions { 24 | o.App = "tv" 25 | return o 26 | } 27 | 28 | func (o *QrcodeOptions) LoginWechatMiniApp() *QrcodeOptions { 29 | o.App = "wechatmini" 30 | return o 31 | } 32 | 33 | func (o *QrcodeOptions) LoginAlipayMiniApp() *QrcodeOptions { 34 | o.App = "alipaymini" 35 | return o 36 | } 37 | 38 | func (o *QrcodeOptions) LoginQandroid() *QrcodeOptions { 39 | o.App = "qandroid" 40 | return o 41 | } 42 | 43 | func Qrcode() *QrcodeOptions { 44 | return (&QrcodeOptions{}).LoginWeb() 45 | } 46 | -------------------------------------------------------------------------------- /order.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import "github.com/deadblue/elevengo/lowlevel/api" 4 | 5 | type FileOrder int 6 | 7 | const ( 8 | FileOrderByName FileOrder = iota 9 | FileOrderBySize 10 | FileOrderByType 11 | FileOrderByCreateTime 12 | FileOrderByUpdateTime 13 | FileOrderByOpenTime 14 | ) 15 | 16 | var ( 17 | fileOrderNames = []string{ 18 | api.FileOrderByName, 19 | api.FileOrderBySize, 20 | api.FileOrderByType, 21 | api.FileOrderByCreateTime, 22 | api.FileOrderByUpdateTime, 23 | api.FileOrderByOpenTime, 24 | } 25 | fileOrderCount = len(fileOrderNames) 26 | ) 27 | 28 | func getOrderName(order FileOrder) string { 29 | if order < 0 || int(order) >= fileOrderCount { 30 | return api.FileOrderDefault 31 | } 32 | return fileOrderNames[order] 33 | } 34 | -------------------------------------------------------------------------------- /plugin/doc.go: -------------------------------------------------------------------------------- 1 | // Package plugin declares some interfaces that allow developer customizing elevengo agent. 2 | package plugin 3 | -------------------------------------------------------------------------------- /plugin/http.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | import "net/http" 4 | 5 | // HttpClient declares the interface which an HTTP client should 6 | // implement, a well-known implementation is `http.Client`, also 7 | // developer can implement it by himself. 8 | type HttpClient interface { 9 | 10 | // Do sends HTTP request and returns HTTP responses. 11 | Do(req *http.Request) (resp *http.Response, err error) 12 | } 13 | 14 | // HttpClientWithJar declares interface for developer, who uses 15 | // self-implemented HttpClient instead of `http.Client`, and 16 | // manages cookie himself. 17 | type HttpClientWithJar interface { 18 | HttpClient 19 | 20 | // Jar returns client managed cookie jar. 21 | Jar() http.CookieJar 22 | } 23 | -------------------------------------------------------------------------------- /plugin/logger.go: -------------------------------------------------------------------------------- 1 | package plugin 2 | 3 | /* 4 | Logger interface for printing debug message. 5 | 6 | Caller can implement himself, or simply uses log.Logger in stdlib. 7 | */ 8 | type Logger interface { 9 | 10 | // Println prints message. 11 | // The message does not end with newline character("\n"), implementation should append one. 12 | Println(v ...interface{}) 13 | } 14 | -------------------------------------------------------------------------------- /qrcode.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/deadblue/elevengo/internal/util" 10 | "github.com/deadblue/elevengo/lowlevel/api" 11 | "github.com/deadblue/elevengo/option" 12 | ) 13 | 14 | const ( 15 | formatQrcodeSession = "%s|%s|%d|%s" 16 | ) 17 | 18 | // QrcodeSession holds the information during a QRCode login process. 19 | type QrcodeSession struct { 20 | // QRCode image content. 21 | Image []byte 22 | // Hidden fields. 23 | app string 24 | uid string 25 | time int64 26 | sign string 27 | } 28 | 29 | // Marshal marshals QrcodeSession into a string, which can be tranfered out of 30 | // process. 31 | func (s *QrcodeSession) Marshal() string { 32 | if s.time == 0 { 33 | return "" 34 | } 35 | return fmt.Sprintf(formatQrcodeSession, s.app, s.uid, s.time, s.sign) 36 | } 37 | 38 | // Unmarshal fills QrcodeSession from a string which is generated by |Marshal|. 39 | func (s *QrcodeSession) Unmarshal(text string) (err error) { 40 | _, err = fmt.Sscanf( 41 | text, formatQrcodeSession, 42 | &s.app, &s.uid, &s.time, &s.sign, 43 | ) 44 | return 45 | } 46 | 47 | var ErrQrcodeCancelled = errors.New("QRcode cancelled") 48 | 49 | // QrcodeStart starts a QRcode sign-in session. 50 | // The session is for web by default, you can change sign-in app by passing a 51 | // "option.QrcodeLoginOption". 52 | // 53 | // Example: 54 | // 55 | // agent := elevengo.Default() 56 | // session := elevengo.QrcodeSession() 57 | // agent.QrcodeStart(session, option.Qrcode().LoginTv()) 58 | func (a *Agent) QrcodeStart(session *QrcodeSession, options ...*option.QrcodeOptions) (err error) { 59 | // Apply options 60 | app := "web" 61 | if opts := util.NotNull(options...); opts != nil { 62 | app = opts.App 63 | } 64 | // Get token 65 | spec := (&api.QrcodeTokenSpec{}).Init(app) 66 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 67 | return 68 | } 69 | session.app = app 70 | session.uid = spec.Result.Uid 71 | session.time = spec.Result.Time 72 | session.sign = spec.Result.Sign 73 | // Fetch QRcode image data 74 | var reader io.ReadCloser 75 | if reader, err = a.Fetch(api.QrcodeImageUrl(session.uid)); err != nil { 76 | return 77 | } 78 | defer util.QuietlyClose(reader) 79 | session.Image, err = io.ReadAll(reader) 80 | return 81 | } 82 | 83 | func (a *Agent) qrcodeSignIn(session *QrcodeSession) (err error) { 84 | spec := (&api.QrcodeLoginSpec{}).Init(session.app, session.uid) 85 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 86 | return 87 | } 88 | return a.afterSignIn(spec.Result.Cookie.UID) 89 | } 90 | 91 | // QrcodePoll polls the session state, and automatically sin 92 | func (a *Agent) QrcodePoll(session *QrcodeSession) (done bool, err error) { 93 | spec := (&api.QrcodeStatusSpec{}).Init( 94 | session.uid, session.time, session.sign, 95 | ) 96 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 97 | return 98 | } 99 | switch spec.Result.Status { 100 | case -2: 101 | err = ErrQrcodeCancelled 102 | case 2: 103 | err = a.qrcodeSignIn(session) 104 | done = err == nil 105 | } 106 | return 107 | } 108 | -------------------------------------------------------------------------------- /star.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deadblue/elevengo/lowlevel/api" 7 | ) 8 | 9 | // FileSetStar adds/removes star from a file, whose ID is fileId. 10 | func (a *Agent) FileSetStar(fileId string, star bool) (err error) { 11 | spec := (&api.FileStarSpec{}).Init(fileId, star) 12 | return a.llc.CallApi(spec, context.Background()) 13 | } 14 | -------------------------------------------------------------------------------- /storage.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/deadblue/elevengo/lowlevel/api" 7 | ) 8 | 9 | // StorageInfo describes storage space usage. 10 | type StorageInfo struct { 11 | // Total size in bytes. 12 | Size int64 13 | // Human-readable total size. 14 | FormatSize string 15 | 16 | // Used size in bytes. 17 | Used int64 18 | // Human-readable used size. 19 | FormatUsed string 20 | 21 | // Available size in bytes. 22 | Avail int64 23 | // Human-readable remain size. 24 | FormatAvail string 25 | } 26 | 27 | // StorageStat gets storage size information. 28 | func (a *Agent) StorageStat(info *StorageInfo) (err error) { 29 | spec := (&api.IndexInfoSpec{}).Init() 30 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 31 | return 32 | } 33 | space := spec.Result.SpaceInfo 34 | info.Size = int64(space.Total.Size) 35 | info.Used = int64(space.Used.Size) 36 | info.Avail = int64(space.Remain.Size) 37 | info.FormatSize = space.Total.SizeFormat 38 | info.FormatUsed = space.Used.SizeFormat 39 | info.FormatAvail = space.Remain.SizeFormat 40 | return 41 | } 42 | -------------------------------------------------------------------------------- /upload.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | "net/http" 8 | "strconv" 9 | "time" 10 | 11 | "github.com/deadblue/elevengo/internal/crypto/hash" 12 | "github.com/deadblue/elevengo/internal/oss" 13 | "github.com/deadblue/elevengo/internal/protocol" 14 | "github.com/deadblue/elevengo/internal/util" 15 | "github.com/deadblue/elevengo/lowlevel/api" 16 | "github.com/deadblue/elevengo/lowlevel/errors" 17 | "github.com/deadblue/elevengo/lowlevel/types" 18 | ) 19 | 20 | // UploadTicket contains all required information to upload a file. 21 | type UploadTicket struct { 22 | // Expiration time 23 | Expiration time.Time 24 | // Is file exists 25 | Exist bool 26 | // Request method 27 | Verb string 28 | // Remote URL which will receive the file content. 29 | Url string 30 | // Request header 31 | Header map[string]string 32 | } 33 | 34 | func (t *UploadTicket) header(name, value string) *UploadTicket { 35 | t.Header[name] = value 36 | return t 37 | } 38 | 39 | type _UploadOssParams struct { 40 | // File size 41 | Size int64 42 | // File MD5 hash in base64-encoding 43 | MD5 string 44 | // File SHA-1 hash in hex-encoding 45 | SHA1 string 46 | // Bucket name on OSS 47 | Bucket string 48 | // Object name on OSS 49 | Object string 50 | // Callback parameters 51 | Callback string 52 | CallbackVar string 53 | } 54 | 55 | func (a *Agent) uploadInit( 56 | dirId, name string, 57 | rs io.ReadSeeker, maxSize int64, 58 | op *_UploadOssParams, 59 | ) (exists bool, err error) { 60 | // Digest incoming stream 61 | dr := &hash.DigestResult{} 62 | if err = hash.Digest(rs, dr); err != nil { 63 | return 64 | } 65 | if maxSize > 0 && dr.Size > maxSize { 66 | err = errors.ErrUploadTooLarge 67 | return 68 | } 69 | var signKey, signValue string 70 | // Call API 71 | for { 72 | spec := (&api.UploadInitSpec{}).Init( 73 | dirId, dr.SHA1, name, dr.Size, signKey, signValue, &a.common, 74 | ) 75 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 76 | break 77 | } 78 | if spec.Result.SignKey != "" { 79 | // Update parameters 80 | signKey = spec.Result.SignKey 81 | signValue, _ = hash.DigestRange(rs, spec.Result.SignCheck) 82 | } else { 83 | if spec.Result.Exists { 84 | exists = true 85 | } else if op != nil { 86 | op.Size = dr.Size 87 | op.MD5 = dr.MD5 88 | op.SHA1 = dr.SHA1 89 | op.Bucket = spec.Result.Oss.Bucket 90 | op.Object = spec.Result.Oss.Object 91 | op.Callback = spec.Result.Oss.Callback 92 | op.CallbackVar = spec.Result.Oss.CallbackVar 93 | } 94 | break 95 | } 96 | } 97 | return 98 | } 99 | 100 | // UploadCreateTicket creates a ticket which contains all required parameters 101 | // to upload file/data to cloud, the ticket should be used in 1 hour. 102 | // 103 | // To create ticket, r will be fully read to calculate SHA-1 and MD5 hash value. 104 | // If you want to re-use r, try to seek it to beginning. 105 | // 106 | // To upload a file larger than 5G bytes, use `UploadCreateOssTicket`. 107 | func (a *Agent) UploadCreateTicket( 108 | dirId, name string, r io.ReadSeeker, ticket *UploadTicket, 109 | ) (err error) { 110 | // Initialize uploading 111 | op := &_UploadOssParams{} 112 | if ticket.Exist, err = a.uploadInit( 113 | dirId, name, r, api.UploadMaxSize, op, 114 | ); err != nil || ticket.Exist { 115 | return 116 | } 117 | // Get OSS token 118 | spec := (&api.UploadTokenSpec{}).Init() 119 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 120 | return 121 | } 122 | // Fill UploadTicket 123 | ticket.Expiration = spec.Result.Expiration 124 | ticket.Verb = http.MethodPut 125 | ticket.Url = oss.GetPutObjectUrl(op.Bucket, op.Object) 126 | if ticket.Header == nil { 127 | ticket.Header = make(map[string]string) 128 | } 129 | ticket.header(oss.HeaderDate, oss.Date()). 130 | header(oss.HeaderContentLength, strconv.FormatInt(op.Size, 10)). 131 | header(oss.HeaderContentType, util.DetermineMimeType(name)). 132 | header(oss.HeaderContentMd5, op.MD5). 133 | header(oss.HeaderOssCallback, util.Base64Encode(op.Callback)). 134 | header(oss.HeaderOssCallbackVar, util.Base64Encode(op.CallbackVar)). 135 | header(oss.HeaderOssSecurityToken, spec.Result.SecurityToken) 136 | 137 | authorization := oss.CalculateAuthorization(&oss.RequestMetadata{ 138 | Verb: ticket.Verb, 139 | Header: ticket.Header, 140 | Bucket: op.Bucket, 141 | Object: op.Object, 142 | }, spec.Result.AccessKeyId, spec.Result.AccessKeySecret) 143 | ticket.header(oss.HeaderAuthorization, authorization) 144 | return 145 | } 146 | 147 | // UploadOssTicket contains all required paramters to upload a file through 148 | // aliyun-oss-sdk(https://github.com/aliyun/aliyun-oss-go-sdk). 149 | type UploadOssTicket struct { 150 | // Expiration time 151 | Expiration time.Time 152 | // Is file already exists 153 | Exist bool 154 | // Client parameters 155 | Client struct { 156 | Region string 157 | Endpoint string 158 | AccessKeyId string 159 | AccessKeySecret string 160 | SecurityToken string 161 | } 162 | // Bucket name 163 | Bucket string 164 | // Object key 165 | Object string 166 | // Callback option 167 | Callback string 168 | // CallbackVar option 169 | CallbackVar string 170 | } 171 | 172 | /* 173 | UploadCreateOssTicket creates ticket to upload file through aliyun-oss-sdk. Use 174 | this method if you want to upload a file larger than 5G bytes. 175 | 176 | To create ticket, r will be fully read to calculate SHA-1 and MD5 hash value. 177 | If you want to re-use r, try to seek it to beginning. 178 | 179 | Example: 180 | 181 | import ( 182 | "github.com/aliyun/aliyun-oss-go-sdk/oss" 183 | "github.com/deadblue/elevengo" 184 | ) 185 | 186 | func main() { 187 | filePath := "/file/to/upload" 188 | 189 | var err error 190 | 191 | file, err := os.Open(filePath) 192 | if err != nil { 193 | log.Fatalf("Open file failed: %s", err) 194 | } 195 | defer file.Close() 196 | 197 | // Create 115 agent 198 | agent := elevengo.Default() 199 | if err = agent.CredentialImport(&elevengo.Credential{ 200 | UID: "", CID: "", SEID: "", 201 | }); err != nil { 202 | log.Fatalf("Login failed: %s", err) 203 | } 204 | // Prepare OSS upload ticket 205 | ticket := &UploadOssTicket{} 206 | if err = agent.UploadCreateOssTicket( 207 | "dirId", 208 | filepath.Base(file.Name()), 209 | file, 210 | ticket, 211 | ); err != nil { 212 | log.Fatalf("Create OSS ticket failed: %s", err) 213 | } 214 | if ticket.Exist { 215 | log.Printf("File has been fast-uploaded!") 216 | return 217 | } 218 | 219 | // Create OSS client 220 | oc, err := oss.New( 221 | ticket.Client.Endpoint, 222 | ticket.Client.AccessKeyId, 223 | ticket.Client.AccessKeySecret, 224 | oss.SecurityToken(ticket.Client.SecurityToken) 225 | ) 226 | if err != nil { 227 | log.Fatalf("Create OSS client failed: %s", err) 228 | } 229 | bucket, err := oc.Bucket(ticket.Bucket) 230 | if err != nil { 231 | log.Fatalf("Get OSS bucket failed: %s", err) 232 | } 233 | // Upload file in multipart. 234 | err = bucket.UploadFile( 235 | ticket.Object, 236 | filePath, 237 | 100 * 1024 * 1024, // 100 Megabytes per part 238 | oss.Callback(ticket.Callback), 239 | oss.CallbackVar(ticket.CallbackVar), 240 | ) 241 | // Until now (2023-01-29), there is a bug in aliyun-oss-go-sdk: 242 | // When set Callback option, the response from CompleteMultipartUpload API 243 | // is returned by callback host, which is not the standard XML. But SDK 244 | // always tries to parse it as CompleteMultipartUploadResult, and returns 245 | // `io.EOF` error, just ignore it! 246 | if err != nil && err != io.EOF { 247 | log.Fatalf("Upload file failed: %s", err) 248 | } else { 249 | log.Print("Upload done!") 250 | } 251 | } 252 | */ 253 | func (a *Agent) UploadCreateOssTicket( 254 | dirId, name string, r io.ReadSeeker, ticket *UploadOssTicket, 255 | ) (err error) { 256 | // Get OSS parameters 257 | op := &_UploadOssParams{} 258 | if ticket.Exist, err = a.uploadInit( 259 | dirId, name, r, -1, op, 260 | ); err != nil || ticket.Exist { 261 | return 262 | } 263 | // Get OSS token 264 | spec := (&api.UploadTokenSpec{}).Init() 265 | if err = a.llc.CallApi(spec, context.Background()); err != nil { 266 | return 267 | } 268 | // Fill ticket 269 | ticket.Expiration = spec.Result.Expiration 270 | ticket.Client.Region = oss.Region 271 | ticket.Client.Endpoint = oss.GetEndpointUrl() 272 | ticket.Client.AccessKeyId = spec.Result.AccessKeyId 273 | ticket.Client.AccessKeySecret = spec.Result.AccessKeySecret 274 | ticket.Client.SecurityToken = spec.Result.SecurityToken 275 | ticket.Bucket = op.Bucket 276 | ticket.Object = op.Object 277 | ticket.Callback = util.Base64Encode( 278 | oss.ReplaceCallbackSha1(op.Callback, op.SHA1), 279 | ) 280 | ticket.CallbackVar = util.Base64Encode(op.CallbackVar) 281 | return 282 | } 283 | 284 | // UploadParseResult parses the raw upload response, and fills it to file. 285 | func (a *Agent) UploadParseResult(r io.Reader, file *File) (err error) { 286 | jd, resp := json.NewDecoder(r), &protocol.StandardResp{} 287 | if err = jd.Decode(resp); err == nil { 288 | err = resp.Err() 289 | } 290 | if err != nil || file == nil { 291 | return 292 | } 293 | result := &types.UploadSampleResult{} 294 | if err = resp.Extract(result); err != nil { 295 | return 296 | } 297 | // Note: Not all fields of file are filled. 298 | file.IsDirectory = false 299 | file.FileId = result.FileId 300 | file.Name = result.FileName 301 | file.Size = result.FileSize.Int64() 302 | file.Sha1 = result.FileSha1 303 | file.PickCode = result.PickCode 304 | return 305 | } 306 | 307 | // UploadSample directly uploads small file/data (smaller than 200MB) to cloud. 308 | func (a *Agent) UploadSample(dirId, name string, size int64, r io.Reader) (fileId string, err error) { 309 | if size == 0 { 310 | size = util.GuessSize(r) 311 | } 312 | // Check upload size 313 | if size <= 0 { 314 | // What the fuck? 315 | return "", errors.ErrUploadNothing 316 | } else if size > api.UploadMaxSizeSample { 317 | return "", errors.ErrUploadTooLarge 318 | } 319 | // Call API. 320 | initSpec := (&api.UploadSampleInitSpec{}).Init(dirId, name, size, &a.common) 321 | if err = a.llc.CallApi(initSpec, context.Background()); err != nil { 322 | return 323 | } 324 | // Upload file 325 | upSpec := (&api.UploadSampleSpec{}).Init(dirId, name, size, r, &initSpec.Result) 326 | if err = a.llc.CallApi(upSpec, context.Background()); err == nil { 327 | fileId = upSpec.Result.FileId 328 | } 329 | return 330 | } 331 | -------------------------------------------------------------------------------- /version.go: -------------------------------------------------------------------------------- 1 | package elevengo 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | ) 7 | 8 | const ( 9 | libName = "elevengo" 10 | libVer = "0.7.8" 11 | ) 12 | 13 | var ( 14 | version = fmt.Sprintf("%s %s (%s %s/%s)", 15 | libName, libVer, runtime.Version(), runtime.GOOS, runtime.GOARCH) 16 | ) 17 | 18 | func (a *Agent) Version() string { 19 | return version 20 | } 21 | --------------------------------------------------------------------------------