diff --git a/dto.go b/dto.go new file mode 100644 index 0000000..e97b285 --- /dev/null +++ b/dto.go @@ -0,0 +1,74 @@ +package main + +type Config struct { + Port int + Token string + AuthKey string +} + +type Lang struct { + SourceLangUserSelected string `json:"source_lang_user_selected"` + TargetLang string `json:"target_lang"` +} + +type CommonJobParams struct { + WasSpoken bool `json:"wasSpoken"` + TranscribeAS string `json:"transcribe_as"` + // RegionalVariant string `json:"regionalVariant"` +} + +type Params struct { + Texts []Text `json:"texts"` + Splitting string `json:"splitting"` + Lang Lang `json:"lang"` + Timestamp int64 `json:"timestamp"` + CommonJobParams CommonJobParams `json:"commonJobParams"` +} + +type Text struct { + Text string `json:"text"` + RequestAlternatives int `json:"requestAlternatives"` +} + +type PostData struct { + Jsonrpc string `json:"jsonrpc"` + Method string `json:"method"` + ID int64 `json:"id"` + Params Params `json:"params"` +} + +type PayloadFree struct { + TransText string `json:"text"` + SourceLang string `json:"source_lang"` + TargetLang string `json:"target_lang"` +} + +type PayloadAPI struct { + Text []string `json:"text"` + TargetLang string `json:"target_lang"` + SourceLang string `json:"source_lang"` +} + +type Translation struct { + Text string `json:"text"` +} + +type TranslationResponse struct { + Translations []Translation `json:"translations"` +} + +type DeepLUsageResponse struct { + CharacterCount int `json:"character_count"` + CharacterLimit int `json:"character_limit"` +} + +type DeepLXTranslationResult struct { + Code int + ID int64 + Message string + Data string + Alternatives []string + SourceLang string + TargetLang string + Method string +} diff --git a/go.mod b/go.mod index 3612eab..01c1bed 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/OwO-Network/DeepLX -go 1.19 +go 1.22.1 require ( github.com/abadojack/whatlanggo v1.0.1 diff --git a/main.go b/main.go index 5995e99..299ec34 100644 --- a/main.go +++ b/main.go @@ -1,14 +1,14 @@ /* - * @Author: Vincent Young + * @Author: Vincent Yang * @Date: 2023-07-01 21:45:34 - * @LastEditors: Vincent Young - * @LastEditTime: 2023-12-08 19:00:47 + * @LastEditors: Vincent Yang + * @LastEditTime: 2024-03-20 16:39:58 * @FilePath: /DeepLX/main.go * @Telegram: https://t.me/missuo + * @GitHub: https://github.com/missuo * - * Copyright © 2023 by Vincent, All Rights Reserved. + * Copyright © 2024 by Vincent, All Rights Reserved. */ - package main import ( @@ -17,7 +17,6 @@ import ( "flag" "fmt" "io" - "io/ioutil" "log" "math/rand" "net/http" @@ -32,13 +31,7 @@ import ( "github.com/tidwall/gjson" ) -type Config struct { - Port int - Token string - AuthKey string -} - -func InitConfig() *Config { +func initConfig() *Config { cfg := &Config{ Port: 1188, } @@ -64,38 +57,7 @@ func InitConfig() *Config { return cfg } -type Lang struct { - SourceLangUserSelected string `json:"source_lang_user_selected"` - TargetLang string `json:"target_lang"` -} - -type CommonJobParams struct { - WasSpoken bool `json:"wasSpoken"` - TranscribeAS string `json:"transcribe_as"` - // RegionalVariant string `json:"regionalVariant"` -} - -type Params struct { - Texts []Text `json:"texts"` - Splitting string `json:"splitting"` - Lang Lang `json:"lang"` - Timestamp int64 `json:"timestamp"` - CommonJobParams CommonJobParams `json:"commonJobParams"` -} - -type Text struct { - Text string `json:"text"` - RequestAlternatives int `json:"requestAlternatives"` -} - -type PostData struct { - Jsonrpc string `json:"jsonrpc"` - Method string `json:"method"` - ID int64 `json:"id"` - Params Params `json:"params"` -} - -func initData(sourceLang string, targetLang string) *PostData { +func initDeepLXData(sourceLang string, targetLang string) *PostData { return &PostData{ Jsonrpc: "2.0", Method: "LMT_handle_texts", @@ -119,8 +81,9 @@ func getICount(translateText string) int64 { } func getRandomNumber() int64 { - rand.Seed(time.Now().Unix()) - num := rand.Int63n(99999) + 8300000 + src := rand.NewSource(time.Now().UnixNano()) + rng := rand.New(src) + num := rng.Int63n(99999) + 8300000 return num * 1000 } @@ -134,27 +97,36 @@ func getTimeStamp(iCount int64) int64 { } } -type PayloadFree struct { - TransText string `json:"text"` - SourceLang string `json:"source_lang"` - TargetLang string `json:"target_lang"` +func checkUsageAuthKey(authKey string) (bool, error) { + url := "https://api-free.deepl.com/v2/usage" + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return false, err + } + + req.Header.Add("Authorization", "DeepL-Auth-Key "+authKey) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return false, err + } + + var response DeepLUsageResponse + err = json.Unmarshal(body, &response) + if err != nil { + return false, err + } + return response.CharacterCount < 499900, nil } -type PayloadAPI struct { - Text []string `json:"text"` - TargetLang string `json:"target_lang"` - SourceLang string `json:"source_lang"` -} - -type Translation struct { - Text string `json:"text"` -} - -type TranslationResponse struct { - Translations []Translation `json:"translations"` -} - -func translateByAPI(text string, sourceLang string, targetLang string, authKey string) (string, error) { +func translateByOfficialAPI(text string, sourceLang string, targetLang string, authKey string) (string, error) { url := "https://api-free.deepl.com/v2/translate" textArray := strings.Split(text, "\n") @@ -184,7 +156,7 @@ func translateByAPI(text string, sourceLang string, targetLang string, authKey s } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { return "", err } @@ -205,42 +177,162 @@ func translateByAPI(text string, sourceLang string, targetLang string, authKey s return sb.String(), nil } -type DeepLResponse struct { - CharacterCount int `json:"character_count"` - CharacterLimit int `json:"character_limit"` -} - -func checkUsage(authKey string) (bool, error) { - url := "https://api-free.deepl.com/v2/usage" - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return false, err +func translateByDeepLX(sourceLang string, targetLang string, translateText string, authKey string) (DeepLXTranslationResult, error) { + id := getRandomNumber() + if sourceLang == "" { + lang := whatlanggo.DetectLang(translateText) + deepLLang := strings.ToUpper(lang.Iso6391()) + sourceLang = deepLLang + } + // If target language is not specified, set it to English + if targetLang == "" { + targetLang = "EN" + } + // Handling empty translation text + if translateText == "" { + return DeepLXTranslationResult{ + Code: http.StatusNotFound, + Message: "No text to translate", + }, nil } - req.Header.Add("Authorization", "DeepL-Auth-Key "+authKey) + // Preparing the request data for the DeepL API + url := "https://www2.deepl.com/jsonrpc" + id = id + 1 + postData := initDeepLXData(sourceLang, targetLang) + text := Text{ + Text: translateText, + RequestAlternatives: 3, + } + postData.ID = id + postData.Params.Texts = append(postData.Params.Texts, text) + postData.Params.Timestamp = getTimeStamp(getICount(translateText)) + + // Marshalling the request data to JSON and making necessary string replacements + post_byte, _ := json.Marshal(postData) + postStr := string(post_byte) + + // Adding spaces to the JSON string based on the ID to adhere to DeepL's request formatting rules + if (id+5)%29 == 0 || (id+3)%13 == 0 { + postStr = strings.Replace(postStr, "\"method\":\"", "\"method\" : \"", -1) + } else { + postStr = strings.Replace(postStr, "\"method\":\"", "\"method\": \"", -1) + } + + // Creating a new HTTP POST request with the JSON data as the body + post_byte = []byte(postStr) + reader := bytes.NewReader(post_byte) + request, err := http.NewRequest("POST", url, reader) - client := &http.Client{} - resp, err := client.Do(req) if err != nil { - return false, err + log.Println(err) + return DeepLXTranslationResult{ + Code: http.StatusServiceUnavailable, + Message: "Post request failed", + }, nil + } + + // Setting HTTP headers to mimic a request from the DeepL iOS App + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Accept", "*/*") + request.Header.Set("x-app-os-name", "iOS") + request.Header.Set("x-app-os-version", "16.3.0") + request.Header.Set("Accept-Language", "en-US,en;q=0.9") + request.Header.Set("Accept-Encoding", "gzip, deflate, br") + request.Header.Set("x-app-device", "iPhone13,2") + request.Header.Set("User-Agent", "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)") + request.Header.Set("x-app-build", "510265") + request.Header.Set("x-app-version", "2.9.1") + request.Header.Set("Connection", "keep-alive") + + // Making the HTTP request to the DeepL API + client := &http.Client{} + resp, err := client.Do(request) + if err != nil { + log.Println(err) + return DeepLXTranslationResult{ + Code: http.StatusServiceUnavailable, + Message: "DeepL API request failed", + }, nil } defer resp.Body.Close() - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return false, err + // Handling potential Brotli compressed response body + var bodyReader io.Reader + switch resp.Header.Get("Content-Encoding") { + case "br": + bodyReader = brotli.NewReader(resp.Body) + default: + bodyReader = resp.Body } - var response DeepLResponse - err = json.Unmarshal(body, &response) - if err != nil { - return false, err + // Reading the response body and parsing it with gjson + body, _ := io.ReadAll(bodyReader) + // body, _ := io.ReadAll(resp.Body) + res := gjson.ParseBytes(body) + + // Handling various response statuses and potential errors + if res.Get("error.code").String() == "-32600" { + log.Println(res.Get("error").String()) + return DeepLXTranslationResult{ + Code: http.StatusNotAcceptable, + Message: "Invalid target language", + }, nil } - return response.CharacterCount < 499900, nil + + if resp.StatusCode == http.StatusTooManyRequests && authKey != "" { + authKeyArray := strings.Split(authKey, ",") + for _, authKey := range authKeyArray { + validity, err := checkUsageAuthKey(authKey) + if err != nil { + continue + } else { + if validity { + translatedText, err := translateByOfficialAPI(translateText, sourceLang, targetLang, authKey) + if err != nil { + return DeepLXTranslationResult{ + Code: http.StatusTooManyRequests, + Message: "Too Many Requests", + }, nil + } + return DeepLXTranslationResult{ + Code: http.StatusOK, + Message: "Success", + ID: 1000000, + Data: translatedText, + SourceLang: sourceLang, + TargetLang: targetLang, + Method: "Official API", + }, nil + } + } + + } + } else { + var alternatives []string + res.Get("result.texts.0.alternatives").ForEach(func(key, value gjson.Result) bool { + alternatives = append(alternatives, value.Get("text").String()) + return true + }) + return DeepLXTranslationResult{ + Code: http.StatusOK, + ID: id, + Message: "Success", + Data: res.Get("result.texts.0.text").String(), + Alternatives: alternatives, + SourceLang: sourceLang, + TargetLang: targetLang, + Method: "Free", + }, nil + } + return DeepLXTranslationResult{ + Code: http.StatusServiceUnavailable, + Message: "Uknown error", + }, nil } func main() { - cfg := InitConfig() + cfg := initConfig() fmt.Printf("DeepL X has been successfully launched! Listening on 0.0.0.0:%v\n", cfg.Port) fmt.Println("Developed by sjlleo and missuo .") @@ -252,9 +344,6 @@ func main() { fmt.Println("DeepL Official Authentication key is set.") } - // Generating a random ID - id := getRandomNumber() - // Setting the application to release mode gin.SetMode(gin.ReleaseMode) r := gin.Default() @@ -285,149 +374,61 @@ func main() { } } - // Extracting details from the request JSON sourceLang := req.SourceLang targetLang := req.TargetLang translateText := req.TransText + authKey := cfg.AuthKey - // If source language is not specified, auto-detect it - if sourceLang == "" { - lang := whatlanggo.DetectLang(translateText) - deepLLang := strings.ToUpper(lang.Iso6391()) - sourceLang = deepLLang - } - // If target language is not specified, set it to English - if targetLang == "" { - targetLang = "EN" - } - // Handling empty translation text - if translateText == "" { - c.JSON(http.StatusNotFound, gin.H{ - "code": http.StatusNotFound, - "message": "No Translate Text Found", - }) - return - } - // Preparing the request data for the DeepL API - url := "https://www2.deepl.com/jsonrpc" - id = id + 1 - postData := initData(sourceLang, targetLang) - text := Text{ - Text: translateText, - RequestAlternatives: 3, - } - postData.ID = id - postData.Params.Texts = append(postData.Params.Texts, text) - postData.Params.Timestamp = getTimeStamp(getICount(translateText)) - - // Marshalling the request data to JSON and making necessary string replacements - post_byte, _ := json.Marshal(postData) - postStr := string(post_byte) - - // Adding spaces to the JSON string based on the ID to adhere to DeepL's request formatting rules - if (id+5)%29 == 0 || (id+3)%13 == 0 { - postStr = strings.Replace(postStr, "\"method\":\"", "\"method\" : \"", -1) - } else { - postStr = strings.Replace(postStr, "\"method\":\"", "\"method\": \"", -1) - } - - // Creating a new HTTP POST request with the JSON data as the body - post_byte = []byte(postStr) - reader := bytes.NewReader(post_byte) - request, err := http.NewRequest("POST", url, reader) + result, err := translateByDeepLX(sourceLang, targetLang, translateText, authKey) if err != nil { - log.Println(err) - return + log.Fatalf("Translation failed: %s", err) } - // Setting HTTP headers to mimic a request from the DeepL iOS App - request.Header.Set("Content-Type", "application/json") - request.Header.Set("Accept", "*/*") - request.Header.Set("x-app-os-name", "iOS") - request.Header.Set("x-app-os-version", "16.3.0") - request.Header.Set("Accept-Language", "en-US,en;q=0.9") - request.Header.Set("Accept-Encoding", "gzip, deflate, br") - request.Header.Set("x-app-device", "iPhone13,2") - request.Header.Set("User-Agent", "DeepL-iOS/2.9.1 iOS 16.3.0 (iPhone13,2)") - request.Header.Set("x-app-build", "510265") - request.Header.Set("x-app-version", "2.9.1") - request.Header.Set("Connection", "keep-alive") - - // Making the HTTP request to the DeepL API - client := &http.Client{} - resp, err := client.Do(request) - if err != nil { - log.Println(err) - return - } - defer resp.Body.Close() - - // Handling potential Brotli compressed response body - var bodyReader io.Reader - switch resp.Header.Get("Content-Encoding") { - case "br": - bodyReader = brotli.NewReader(resp.Body) - default: - bodyReader = resp.Body - } - - // Reading the response body and parsing it with gjson - body, err := io.ReadAll(bodyReader) - // body, _ := io.ReadAll(resp.Body) - res := gjson.ParseBytes(body) - - // Handling various response statuses and potential errors - if res.Get("error.code").String() == "-32600" { - log.Println(res.Get("error").String()) - c.JSON(http.StatusNotAcceptable, gin.H{ - "code": http.StatusNotAcceptable, - "message": "Invalid targetLang", - }) - return - } - - if resp.StatusCode == http.StatusTooManyRequests { - authKeyArray := strings.Split(cfg.AuthKey, ",") - for _, authKey := range authKeyArray { - validity, err := checkUsage(authKey) - if err != nil { - continue - } else { - if validity == true { - translatedText, err := translateByAPI(translateText, sourceLang, targetLang, authKey) - if err != nil { - c.JSON(http.StatusTooManyRequests, gin.H{ - "code": http.StatusTooManyRequests, - "message": "Too Many Requests", - }) - } - c.JSON(http.StatusOK, gin.H{ - "code": http.StatusOK, - "id": 1000000, - "data": translatedText, - "source_lang": sourceLang, - "target_lang": targetLang, - "method": "Official API", - }) - return - } - } - - } - } else { - var alternatives []string - res.Get("result.texts.0.alternatives").ForEach(func(key, value gjson.Result) bool { - alternatives = append(alternatives, value.Get("text").String()) - return true - }) + if result.Code == http.StatusOK { c.JSON(http.StatusOK, gin.H{ "code": http.StatusOK, - "id": id, - "data": res.Get("result.texts.0.text").String(), - "alternatives": alternatives, - "source_lang": sourceLang, - "target_lang": targetLang, - "method": "Free", + "id": result.ID, + "data": result.Data, + "alternatives": result.Alternatives, + "source_lang": result.SourceLang, + "target_lang": result.TargetLang, + "method": result.Method, + }) + } else { + c.JSON(result.Code, gin.H{ + "code": result.Code, + "message": result.Message, + }) + + } + }) + + r.POST("/v2/translate", func(c *gin.Context) { + authorizationHeader := c.GetHeader("Authorization") + parts := strings.Split(authorizationHeader, " ") + var authKey string + if len(parts) == 2 { + authKey = parts[1] + } + translateText := c.PostForm("text") + targetLang := c.PostForm("target_lang") + result, err := translateByDeepLX("", targetLang, translateText, authKey) + if err != nil { + log.Fatalf("Translation failed: %s", err) + } + if result.Code == http.StatusOK { + c.JSON(http.StatusOK, gin.H{ + "translations": []interface{}{ + map[string]interface{}{ + "detected_source_language": result.SourceLang, + "text": result.Data, + }, + }, + }) + } else { + c.JSON(result.Code, gin.H{ + "code": result.Code, + "message": result.Message, }) } })