Skip to content

Commit 79682fc

Browse files
committed
Add client from yitsushi/aoc
1 parent 1e8c52a commit 79682fc

File tree

8 files changed

+429
-0
lines changed

8 files changed

+429
-0
lines changed

client/README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Advent of Code helper
2+
3+
> You can also get the [JSON] for this private leaderboard. Please
4+
> don't make frequent automated requests to this service - avoid
5+
> sending requests more often than once every 15 minutes (900 seconds).
6+
> If you do this from a script, you'll have to provide your session
7+
> cookie in the request; a fresh session cookie lasts for about a month.
8+
> Timestamps use Unix time.
9+
>
10+
> Source: adventofcode.com
11+
12+
```go
13+
targetDir := fmt.Sprintf("input/day%02d", dayNumber)
14+
targetFile := fmt.Sprintf("%s/input", targetDir)
15+
16+
ensurePath(targetDir)
17+
18+
client := aoc.NewClient(os.Getenv("AOC_SESSION"))
19+
20+
err := client.DownloadAndSaveInput(currentYear, dayNumber, targetFile)
21+
if err != nil {
22+
logrus.Fatal(err.Error())
23+
24+
return
25+
}
26+
```
27+
28+
## Submit a solution
29+
30+
```go
31+
client := aoc.NewClient(os.Getenv("AOC_SESSION"))
32+
33+
valid, err := client.SubmitSolution(currentYear, dayNumber, partNumber, solution)
34+
if err != nil {
35+
fmt.Printf("%s\n", err.Error())
36+
37+
return
38+
}
39+
40+
if valid {
41+
fmt.Println("Done \\o/")
42+
} else {
43+
fmt.Println("Something is wrong :(")
44+
}
45+
```
46+
47+
## Generate file/directory structure from template
48+
49+
Note: All file will be rendered with `.tmpl` extension
50+
and all directory will be created where there is at least
51+
one `.tmpl` file.
52+
53+
```go
54+
type templateVariables struct {
55+
Day int
56+
Root string
57+
}
58+
59+
err := aoc.Scaffold(
60+
templateDir,
61+
fmt.Sprintf("days/day%02d", dayNumber),
62+
templateVariables{
63+
Day: dayNumber,
64+
Root: packageRoot,
65+
},
66+
)
67+
if err != nil {
68+
logrus.Errorln(err)
69+
}
70+
```

client/client.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package client
2+
3+
import (
4+
"net/http"
5+
"time"
6+
7+
"github.com/sirupsen/logrus"
8+
)
9+
10+
// HTTPClient is a simple interface for http.Client.
11+
// Reason: mock client in tests.
12+
type HTTPClient interface {
13+
Do(req *http.Request) (*http.Response, error)
14+
}
15+
16+
// Client for Advent of Code.
17+
type Client struct {
18+
SessionToken string
19+
HTTPClient HTTPClient
20+
21+
logger *logrus.Logger
22+
}
23+
24+
// RequestTimout is the timeout of a request in seconds.
25+
const RequestTimout = 10
26+
27+
// NewClient creates a new client.
28+
func NewClient(token string) Client {
29+
return Client{
30+
SessionToken: token,
31+
HTTPClient: &http.Client{
32+
Transport: nil,
33+
CheckRedirect: nil,
34+
Jar: nil,
35+
Timeout: time.Second * RequestTimout,
36+
},
37+
logger: logrus.New(),
38+
}
39+
}
40+
41+
// LogLevel sets logger level.
42+
func (c *Client) LogLevel(level logrus.Level) {
43+
c.logger.SetLevel(level)
44+
}

client/client_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package client_test
2+
3+
// It's something... for later.

client/download.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package client
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"os"
9+
"time"
10+
)
11+
12+
// DownloadInput downloads the requested input file.
13+
func (c *Client) DownloadInput(year, day int) (io.ReadCloser, error) {
14+
url := fmt.Sprintf("https://adventofcode.com/%d/day/%d/input", year, day)
15+
16+
req, err := http.NewRequestWithContext(context.Background(), "GET", url, nil)
17+
if err != nil {
18+
return nil, fmt.Errorf("unable to create request from context: %w", err)
19+
}
20+
21+
req.AddCookie(&http.Cookie{
22+
Name: "session",
23+
Value: c.SessionToken,
24+
Path: "",
25+
Domain: "",
26+
Expires: time.Time{},
27+
RawExpires: "",
28+
MaxAge: 0,
29+
Secure: false,
30+
HttpOnly: false,
31+
SameSite: 0,
32+
Raw: "",
33+
Unparsed: []string{},
34+
})
35+
36+
resp, err := c.HTTPClient.Do(req)
37+
if err != nil {
38+
return nil, err
39+
}
40+
41+
if resp.StatusCode != http.StatusOK {
42+
return nil, DownloadError{StatusCode: resp.StatusCode}
43+
}
44+
45+
return resp.Body, nil
46+
}
47+
48+
// DownloadAndSaveInput downloads and saves the requested input file.
49+
func (c *Client) DownloadAndSaveInput(year, day int, targetFile string) error {
50+
file, err := c.DownloadInput(year, day)
51+
if err != nil {
52+
return err
53+
}
54+
55+
defer file.Close()
56+
57+
outputFile, err := os.Create(targetFile)
58+
if err != nil {
59+
return fmt.Errorf("unable to create output file: %w", err)
60+
}
61+
62+
defer outputFile.Close()
63+
64+
_, err = io.Copy(outputFile, file)
65+
66+
return fmt.Errorf("unable to copy response into the output file: %w", err)
67+
}

client/error.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package client
2+
3+
import "fmt"
4+
5+
// NetworkError occurs when something went wrong with the HTTP request.
6+
type NetworkError struct {
7+
Original error
8+
}
9+
10+
func (e NetworkError) Error() string {
11+
return fmt.Sprintf("Network error: %s", e.Original.Error())
12+
}
13+
14+
// DownloadError occurs when the AoC server returns with a status code
15+
// other than 200.
16+
type DownloadError struct {
17+
StatusCode int
18+
}
19+
20+
func (e DownloadError) Error() string {
21+
return fmt.Sprintf("Download error: %d", e.StatusCode)
22+
}
23+
24+
// SubmitError occurs when the AoC server returns with a status code
25+
// other than 200.
26+
type SubmitError struct {
27+
StatusCode int
28+
}
29+
30+
func (e SubmitError) Error() string {
31+
return fmt.Sprintf("Submit error: %d", e.StatusCode)
32+
}
33+
34+
// IncorrectAnswerError occurs when the provided answer is not correct.
35+
type IncorrectAnswerError struct {
36+
Hint string
37+
Wait string
38+
}
39+
40+
func (e IncorrectAnswerError) Error() string {
41+
return fmt.Sprintf("Error: %s\n%s", e.Hint, e.Wait)
42+
}

client/io.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package client
2+
3+
import (
4+
"os"
5+
)
6+
7+
func ensureDirectory(path string) {
8+
if _, err := os.Stat(path); os.IsNotExist(err) {
9+
_ = os.Mkdir(path, os.ModePerm)
10+
}
11+
}

client/scaffold.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package client
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"path/filepath"
7+
"strings"
8+
"text/template"
9+
)
10+
11+
// Scaffold generates a Solution directory structure for a day.
12+
func Scaffold(templateDir string, outputDir string, variables interface{}) error {
13+
ensureDirectory(outputDir)
14+
15+
walkErr := filepath.Walk(
16+
templateDir,
17+
func(path string, info os.FileInfo, err error) error {
18+
if err != nil {
19+
return err
20+
}
21+
22+
if info.IsDir() {
23+
ensureDirectory(path)
24+
25+
return nil
26+
}
27+
28+
dir := filepath.Join(
29+
outputDir,
30+
strings.TrimPrefix(
31+
strings.TrimSuffix(path, filepath.Base(path)),
32+
templateDir,
33+
),
34+
)
35+
ensureDirectory(dir)
36+
37+
targetFile := strings.TrimSuffix(filepath.Join(dir, filepath.Base(path)), ".tmpl")
38+
39+
outputFile, err := os.Create(targetFile)
40+
if err != nil {
41+
return fmt.Errorf("unable to create output file: %w", err)
42+
}
43+
44+
defer outputFile.Close()
45+
46+
tmpl, err := template.ParseFiles(path)
47+
if err != nil {
48+
return fmt.Errorf("unable to parse template file: %w", err)
49+
}
50+
51+
tmplErr := tmpl.Execute(outputFile, variables)
52+
if tmplErr != nil {
53+
return fmt.Errorf("template execution failed: %w", tmplErr)
54+
}
55+
56+
return nil
57+
},
58+
)
59+
60+
if walkErr != nil {
61+
return fmt.Errorf("something went wrong with scaffolding: %w", walkErr)
62+
}
63+
64+
return nil
65+
}

0 commit comments

Comments
 (0)