345 lines
8.0 KiB
Go
345 lines
8.0 KiB
Go
// Base code taken from: https://gist.github.com/marians/3b55318106df0e4e648158f1ffb43d38
|
|
|
|
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
// "fmt"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
"encoding/json"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/webview/webview"
|
|
"golang.org/x/oauth2"
|
|
"io/ioutil"
|
|
"strconv"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
"os"
|
|
)
|
|
|
|
var (
|
|
conf *oauth2.Config
|
|
ctx context.Context
|
|
w webview.WebView
|
|
client *http.Client
|
|
)
|
|
|
|
|
|
func saveToken(tok *oauth2.Token, filename string) {
|
|
j, err := json.MarshalIndent(tok, "", "\t")
|
|
if err != nil {
|
|
log.Fatalf("Failed to marshal token: %v", err)
|
|
}
|
|
err = ioutil.WriteFile(filename, j, 0664)
|
|
if err != nil {
|
|
log.Fatalf("Failed to save token: %v", err)
|
|
}
|
|
}
|
|
|
|
func loadToken(filename string) *oauth2.Token {
|
|
data, err := ioutil.ReadFile(filename)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
|
|
var v = new(oauth2.Token)
|
|
err = json.Unmarshal(data, &v)
|
|
|
|
if err != nil {
|
|
log.Fatalf("Failed to unmarshal data: %v", err)
|
|
}
|
|
|
|
return v
|
|
}
|
|
|
|
func giveCode(code string) {
|
|
log.Printf("Code: %s", code)
|
|
// Exchange will do the handshake to retrieve the initial access token.
|
|
tok, err := conf.Exchange(ctx, code)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
log.Printf("Token: %s", tok)
|
|
saveToken(tok, "./.token")
|
|
// The HTTP Client returned by conf.Client will refresh the token as necessary.
|
|
client = conf.Client(ctx, tok)
|
|
|
|
_, err = client.Get("https://embed.gog.com/user/data/games")
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
} else {
|
|
log.Println(color.CyanString("Authentication successful"))
|
|
}
|
|
|
|
w.Navigate(`data:text/html,
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<p><strong>Success!</strong></p>
|
|
<p>You are authenticated and can now return to the CLI.</p>
|
|
</body>
|
|
</html>
|
|
`)
|
|
|
|
time.Sleep(1 * time.Second)
|
|
w.Terminate()
|
|
}
|
|
|
|
func openBrowser(url string) {
|
|
w = webview.New(true)
|
|
defer w.Destroy()
|
|
|
|
w.SetTitle("Login")
|
|
w.SetSize(600,600, webview.HintNone)
|
|
w.Navigate(url)
|
|
|
|
w.Bind("giveCode", giveCode)
|
|
w.Init(`
|
|
var params = new URLSearchParams(window.location.search);
|
|
if (params.has('code')) {
|
|
giveCode(params.get('code'));
|
|
}
|
|
`)
|
|
w.Run()
|
|
}
|
|
|
|
type GameList struct {
|
|
Owned []int `json:"owned"`
|
|
}
|
|
|
|
func getGameList() *GameList {
|
|
res, err := client.Get("https://embed.gog.com/user/data/games")
|
|
if err != nil {
|
|
log.Fatalf("Failed to get game list: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse game body: %v", err)
|
|
}
|
|
|
|
// log.Println(string(body))
|
|
var gl = new(GameList)
|
|
err = json.Unmarshal(body, gl)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse game list response: %v", err)
|
|
}
|
|
|
|
return gl
|
|
}
|
|
|
|
type Product struct {
|
|
Id int `json:"id"`
|
|
Title string `json:"title"`
|
|
Downloads struct {
|
|
Installers [] struct {
|
|
Name string `json:"name"`
|
|
Os string `json:"os"`
|
|
Language string `json:"language"`
|
|
Files [] struct {
|
|
Id string `json:"id"`
|
|
Size int64 `json:"size"`
|
|
Downlink string `json:"downlink"`
|
|
} `json:"files"`
|
|
} `json:"installers"`
|
|
} `json:"downloads`
|
|
}
|
|
|
|
func getProduct(id int) *Product {
|
|
url := "https://api.gog.com/products/" + strconv.Itoa(id) + "?expand=downloads"
|
|
res, err := client.Get(url)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get product info: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse product body: %v", err)
|
|
}
|
|
|
|
//log.Println(string(body))
|
|
var p = new(Product)
|
|
err = json.Unmarshal(body, p)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse product response: %v", err)
|
|
}
|
|
|
|
return p
|
|
}
|
|
|
|
type DownloadLinks struct {
|
|
Downlink string `json:"downlink"`
|
|
Checksum string `json:"checksum"`
|
|
InstallerName string
|
|
Filename string
|
|
Md5 string
|
|
}
|
|
|
|
func (dl *DownloadLinks) String() string {
|
|
return fmt.Sprintf("{downlink: %s, checksum: %s}", dl.Downlink, dl.Checksum)
|
|
}
|
|
|
|
func getDownloadLinks(p *Product) []*DownloadLinks {
|
|
var links []*DownloadLinks
|
|
|
|
for _, i := range p.Downloads.Installers {
|
|
for _, f := range i.Files {
|
|
res, err := client.Get(f.Downlink)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get file download info: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse file download body: %v", err)
|
|
}
|
|
|
|
//log.Println(string(body))
|
|
var dl = new(DownloadLinks)
|
|
err = json.Unmarshal(body, dl)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse file download response: %v", err)
|
|
}
|
|
|
|
dl.InstallerName = sanitizeName(i.Name)
|
|
links = append(links, dl)
|
|
}
|
|
}
|
|
|
|
return links
|
|
}
|
|
|
|
func sanitizeName(name string) string {
|
|
s := strings.Replace(name, " ", "_", -1)
|
|
s = strings.Replace(s, "'", "", -1)
|
|
s = strings.Replace(s, "™", "", -1)
|
|
return s
|
|
}
|
|
|
|
func writeChecksumIfNeeded(dl *DownloadLinks, folder string) {
|
|
// TODO: reuse an existing checksum file somehow
|
|
res, err := client.Get(dl.Checksum)
|
|
if err != nil {
|
|
log.Fatalf("Failed to get file checksum info: %v", err)
|
|
}
|
|
defer res.Body.Close()
|
|
body, err := ioutil.ReadAll(res.Body)
|
|
if err != nil {
|
|
log.Fatalf("Failed to parse file checksum body: %v", err)
|
|
}
|
|
|
|
re := regexp.MustCompile(`<file name="([^"]+)".*md5="([^"]+)`)
|
|
m := re.FindSubmatch(body)
|
|
if m != nil {
|
|
dl.Filename = sanitizeName(string(m[1]))
|
|
dl.Md5 = string(m[2])
|
|
|
|
// err = ioutil.WriteFile(folder + "/" + sanitizeName(dl.InstallerName) + "/" + dl.Filename + ".md5", m[2], 0664)
|
|
// if err != nil {
|
|
// log.Fatalf("Failed to write the checksum file: %v", err)
|
|
// }
|
|
}
|
|
}
|
|
|
|
|
|
func updateBashScriptProductFolder(p *Product, folder string, scriptName string) {
|
|
|
|
f, err := os.OpenFile("./" + scriptName, os.O_APPEND|os.O_WRONLY, 0664)
|
|
if err != nil {
|
|
log.Fatalf("Failed to open the script file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString("mkdir -p " + folder + "/" + sanitizeName(p.Title) + "\n")
|
|
if err != nil {
|
|
log.Fatalf("Failed to update the script: %v", err)
|
|
}
|
|
}
|
|
|
|
|
|
func writeBashScript(dl *DownloadLinks, folder string, scriptName string) {
|
|
line := "wget --no-clobber --continue --quiet --show-progress -O \"" + folder + "/" + sanitizeName(dl.InstallerName) + "/" + dl.Filename + "\" \"" + dl.Downlink + "\"\n"
|
|
|
|
f, err := os.OpenFile("./" + scriptName, os.O_APPEND|os.O_WRONLY, 0664)
|
|
if err != nil {
|
|
log.Fatalf("Failed to open the script file: %v", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = f.WriteString(line)
|
|
if err != nil {
|
|
log.Fatalf("Failed to update the script: %v", err)
|
|
}
|
|
}
|
|
|
|
func login(url string) {
|
|
log.Println(color.CyanString("You will now be taken to your browser for authentication"))
|
|
time.Sleep(1 * time.Second)
|
|
openBrowser(url)
|
|
}
|
|
|
|
func main() {
|
|
productsFolder := "./products"
|
|
scriptName := "grab_stuff.sh"
|
|
ctx = context.Background()
|
|
conf = &oauth2.Config{
|
|
ClientID: "46899977096215655",
|
|
ClientSecret: "9d85c43b1482497dbbce61f6e4aa173a433796eeae2ca8c5f6129f2dc4de46d9",
|
|
Scopes: []string{},
|
|
Endpoint: oauth2.Endpoint{
|
|
AuthURL: "https://auth.gog.com/auth",
|
|
TokenURL: "https://auth.gog.com/token",
|
|
},
|
|
// my own callback URL
|
|
RedirectURL: "https://embed.gog.com/on_login_success?origin=client",
|
|
}
|
|
|
|
// add transport
|
|
tr := &http.Transport{
|
|
TLSClientConfig: &tls.Config{},
|
|
}
|
|
sslcli := &http.Client{Transport: tr}
|
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, sslcli)
|
|
|
|
// Redirect user to consent page to ask for permission
|
|
// for the scopes specified above.
|
|
url := conf.AuthCodeURL("state", oauth2.AccessTypeOffline)
|
|
|
|
tok := loadToken("./.token")
|
|
if tok != nil {
|
|
client = conf.Client(ctx, tok)
|
|
|
|
_, err := client.Get("https://embed.gog.com/user/data/games")
|
|
if err != nil {
|
|
login(url)
|
|
} else {
|
|
log.Println(color.CyanString("Authentication successful"))
|
|
}
|
|
} else {
|
|
login(url)
|
|
}
|
|
|
|
gl := getGameList()
|
|
log.Println(gl.Owned)
|
|
ioutil.WriteFile("./" + scriptName, []byte("#!/bin/bash\n"), 0775)
|
|
|
|
for index, id := range gl.Owned {
|
|
log.Printf("Inspecting product: %d of %d - %d", index, len(gl.Owned), id)
|
|
p := getProduct(id)
|
|
dl := getDownloadLinks(p)
|
|
updateBashScriptProductFolder(p, productsFolder, scriptName)
|
|
|
|
for _, d := range dl {
|
|
writeChecksumIfNeeded(d, productsFolder + "/" + sanitizeName(p.Title))
|
|
writeBashScript(d, productsFolder, scriptName)
|
|
}
|
|
}
|
|
|
|
}
|
|
|