├── .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 | 
4 | [](https://pkg.go.dev/github.com/deadblue/elevengo)
5 | 
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 |
--------------------------------------------------------------------------------