wip: initial commit

Extremely unfinished and work in progress.
This commit is contained in:
2022-09-07 08:37:01 +01:00
commit b0e52fc498
13 changed files with 809 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.env*
bin/*
mj2n

94
.golangci.yml Normal file
View File

@@ -0,0 +1,94 @@
linters-settings:
funlen:
lines: 100
statements: 150
gocyclo:
min-complexity: 20
golint:
min-confidence: 0
govet:
check-shadowing: true
enable-all: true
disable:
- fieldalignment
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
linters:
disable-all: true
enable:
- asciicheck
- bodyclose
- depguard
- durationcheck
- errcheck
- errorlint
- exhaustive
- exportloopref
- funlen
- gochecknoinits
- goconst
- gocritic
- gocyclo
- godot
- gofumpt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- importas
- ineffassign
- lll
- misspell
- nakedret
- nilerr
- nlreturn
- noctx
- nolintlint
- prealloc
- predeclared
- revive
- rowserrcheck
- sqlclosecheck
- staticcheck
- structcheck
- tparallel
- typecheck
- unconvert
- unparam
- unused
- wastedassign
- whitespace
issues:
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- source: "^//go:generate "
linters:
- lll
- source: "`json:"
linters:
- lll
- source: "`xml:"
linters:
- lll
- source: "`yaml:"
linters:
- lll
run:
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly

23
commands/midjourney.go Normal file
View File

@@ -0,0 +1,23 @@
package commands
import (
"github.com/jimeh/mj2n/midjourney"
"github.com/spf13/cobra"
)
func NewMidjourney(mc *midjourney.Client) (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "midjourney",
Aliases: []string{"mj"},
Short: "MidJourney specific commands",
}
recentJobsCmd, err := NewMidjourneyRecentJobs(mc)
if err != nil {
return nil, err
}
cmd.AddCommand(recentJobsCmd)
return cmd, nil
}

View File

@@ -0,0 +1,90 @@
package commands
import (
"github.com/jimeh/mj2n/midjourney"
"github.com/spf13/cobra"
)
func NewMidjourneyRecentJobs(mc *midjourney.Client) (*cobra.Command, error) {
cmd := &cobra.Command{
Use: "recent-jobs",
Aliases: []string{"jobs", "recent", "rj", "j", "r"},
Short: "List recent jobs",
RunE: midjourneyRecentJobsRunE(mc),
}
cmd.Flags().StringP("format", "f", "", "output format (yaml or json)")
cmd.Flags().IntP("amount", "a", 50, "amount of jobs to list")
cmd.Flags().StringP("type", "t", "", "type of jobs to list")
cmd.Flags().StringP("order", "o", "new", "either \"new\" or \"oldest\"")
cmd.Flags().StringP("user-id", "u", "", "user ID to list jobs for")
cmd.Flags().StringP("page", "p", "", "page to fetch")
cmd.Flags().Bool("dedupe", true, "dedupe results")
return cmd, nil
}
func midjourneyRecentJobsRunE(mc *midjourney.Client) runEFunc {
return func(cmd *cobra.Command, _ []string) error {
fs := cmd.Flags()
q := &midjourney.RecentJobsQuery{}
if v, err := fs.GetInt("amount"); err == nil && v > 0 {
q.Amount = v
}
if v, err := fs.GetString("type"); err == nil && v != "" {
q.JobType = midjourney.JobType(v)
}
if v, err := fs.GetString("order"); err == nil && v != "" {
q.OrderBy = midjourney.Order(v)
}
if v, err := fs.GetString("user-id"); err == nil && v != "" {
q.UserID = v
}
if v, err := fs.GetInt("page"); err == nil && v != 0 {
q.Page = v
}
if v, err := fs.GetBool("dedupe"); err == nil {
q.Dedupe = v
}
rj, err := mc.RecentJobs(cmd.Context(), q)
if err != nil {
return err
}
r := []*MidjourneyJob{}
for _, j := range rj.Jobs {
r = append(r, &MidjourneyJob{
ID: j.ID,
Status: string(j.CurrentStatus),
Type: string(j.Type),
EnqueueTime: j.EnqueueTime,
Prompt: j.Prompt,
ImagePaths: j.ImagePaths,
IsPublished: j.IsPublished,
UserID: j.UserID,
Username: j.Username,
FullCommand: j.FullCommand,
ReferenceJobID: j.ReferenceJobID,
})
}
format := flagString(cmd, "format")
return render(cmd.OutOrStdout(), format, r)
}
}
type MidjourneyJob struct {
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Status string `json:"current_status,omitempty" yaml:"current_status,omitempty"`
Type string `json:"type,omitempty" yaml:"type,omitempty"`
EnqueueTime string `json:"enqueue_time,omitempty" yaml:"enqueue_time,omitempty"`
Prompt string `json:"prompt,omitempty" yaml:"prompt,omitempty"`
ImagePaths []string `json:"image_paths,omitempty" yaml:"image_paths,omitempty"`
IsPublished bool `json:"is_published,omitempty" yaml:"is_published,omitempty"`
UserID string `json:"user_id,omitempty" yaml:"user_id,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
FullCommand string `json:"full_command,omitempty" yaml:"full_command,omitempty"`
ReferenceJobID string `json:"reference_job_id,omitempty" yaml:"reference_job_id,omitempty"`
}

134
commands/mj2n.go Normal file
View File

@@ -0,0 +1,134 @@
package commands
import (
"io"
"os"
"github.com/jimeh/mj2n/midjourney"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"
)
type runEFunc func(cmd *cobra.Command, _ []string) error
func NewMJ2N() (*cobra.Command, error) {
mc, err := midjourney.New(midjourney.WithUserAgent("mj2n/0.0.1-dev"))
if err != nil {
return nil, err
}
cmd := &cobra.Command{
Use: "mj2n",
Short: "MidJourney to Notion importer",
PersistentPreRunE: persistentPreRunE(mc),
}
cmd.PersistentFlags().StringP(
"log-level", "l", "info",
"one of: trace, debug, info, warn, error, fatal, panic",
)
cmd.PersistentFlags().StringP(
"mj-token", "m", "", "MidJourney API token",
)
cmd.PersistentFlags().String(
"mj-api-url", midjourney.DefaultAPIURL.String(), "MidJourney API URL",
)
midjourneyCmd, err := NewMidjourney(mc)
if err != nil {
return nil, err
}
cmd.AddCommand(midjourneyCmd)
return cmd, nil
}
func persistentPreRunE(mc *midjourney.Client) runEFunc {
return func(cmd *cobra.Command, _ []string) error {
err := setupZerolog(cmd)
if err != nil {
return err
}
err = setupMidJourney(cmd, mc)
if err != nil {
return err
}
return nil
}
}
func setupMidJourney(cmd *cobra.Command, mc *midjourney.Client) error {
opts := []midjourney.Option{
midjourney.WithLogger(log.Logger),
}
if f := cmd.Flag("mj-token"); f.Changed {
opts = append(opts, midjourney.WithAuthToken(f.Value.String()))
} else if v := os.Getenv("MIDJOURNEY_TOKEN"); v != "" {
opts = append(opts, midjourney.WithAuthToken(v))
}
apiURL := flagString(cmd, "mj-api-url")
if apiURL == "" {
apiURL = os.Getenv("MIDJOURNEY_API_URL")
}
if apiURL != "" {
opts = append(opts, midjourney.WithAPIURL(apiURL))
}
return mc.Set(opts...)
}
func setupZerolog(cmd *cobra.Command) error {
var levelStr string
if v := os.Getenv("MJ2N_DEBUG"); v != "" {
levelStr = "debug"
} else if v := os.Getenv("MJ2N_LOG_LEVEL"); v != "" {
levelStr = v
}
var out io.Writer = os.Stderr
if cmd != nil {
out = cmd.OutOrStderr()
fl := cmd.Flag("log-level")
if fl != nil && (fl.Changed || levelStr == "") {
levelStr = fl.Value.String()
}
}
if levelStr == "" {
levelStr = "info"
}
level, err := zerolog.ParseLevel(levelStr)
if err != nil {
return err
}
zerolog.SetGlobalLevel(level)
zerolog.TimeFieldFormat = ""
output := zerolog.ConsoleWriter{Out: out}
output.FormatTimestamp = func(i interface{}) string {
return ""
}
log.Logger = zerolog.New(output).With().Timestamp().Logger()
return nil
}
func flagString(cmd *cobra.Command, name string) string {
var r string
if f := cmd.Flag(name); f != nil {
r = f.Value.String()
}
return r
}

52
commands/render.go Normal file
View File

@@ -0,0 +1,52 @@
package commands
import (
"encoding"
"encoding/json"
"io"
"gopkg.in/yaml.v3"
)
func render(w io.Writer, format string, v interface{}) error {
if format == "yaml" || format == "yml" {
return renderYAML(w, v)
}
if format == "json" {
return renderJSON(w, v)
}
if wt, ok := v.(io.WriterTo); ok {
_, err := wt.WriteTo(w)
return err
}
if tm, ok := v.(encoding.TextMarshaler); ok {
b, err := tm.MarshalText()
if err != nil {
return err
}
_, err = w.Write(b)
return err
}
return renderYAML(w, v)
}
func renderYAML(w io.Writer, v interface{}) error {
enc := yaml.NewEncoder(w)
enc.SetIndent(2)
return enc.Encode(v)
}
func renderJSON(w io.Writer, v interface{}) error {
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(v)
}

17
go.mod Normal file
View File

@@ -0,0 +1,17 @@
module github.com/jimeh/mj2n
go 1.19
require (
github.com/rs/zerolog v1.28.0
github.com/spf13/cobra v1.5.0
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.16 // indirect
github.com/spf13/pflag v1.0.5 // indirect
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 // indirect
)

31
go.sum Normal file
View File

@@ -0,0 +1,31 @@
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY=
github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959 h1:qSa+Hg9oBe6UJXrznE+yYvW51V9UbyIj/nj/KpDigo8=
golang.org/x/sys v0.0.0-20220906165534-d0df966e6959/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

36
main.go Normal file
View File

@@ -0,0 +1,36 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"github.com/jimeh/mj2n/commands"
)
func main() {
cmd, err := commands.NewMJ2N()
if err != nil {
fatal(err)
}
ctx, cancel := signal.NotifyContext(
context.Background(),
syscall.SIGINT, syscall.SIGTERM,
)
defer cancel()
err = cmd.ExecuteContext(ctx)
if err != nil {
defer os.Exit(1)
return
}
}
func fatal(err error) {
fmt.Fprintf(os.Stderr, "ERROR: %s\n", err)
os.Exit(1)
}

144
midjourney/client.go Normal file
View File

@@ -0,0 +1,144 @@
package midjourney
import (
"errors"
"fmt"
"net/http"
"net/url"
"strings"
"github.com/rs/zerolog"
)
var (
Err = errors.New("midjourney")
ErrNoAuthToken = fmt.Errorf("%w: no auth token", Err)
ErrInvalidAPIURL = fmt.Errorf("%w: invalid API URL", Err)
ErrInvalidHTTPClient = fmt.Errorf("%w: invalid HTTP client", Err)
ErrResponseStatus = fmt.Errorf("%w: response status", Err)
DefaultAPIURL = url.URL{
Scheme: "https",
Host: "www.midjourney.com",
Path: "/api/",
}
DefaultUserAgent = "go-midjourney/0.0.1-dev"
)
type Order string
const (
OrderNew Order = "new"
OrderOldest Order = "oldest"
)
type Option interface {
apply(*Client) error
}
type optionFunc func(*Client) error
func (fn optionFunc) apply(o *Client) error {
return fn(o)
}
func WithAuthToken(authToken string) Option {
return optionFunc(func(c *Client) error {
c.AuthToken = authToken
return nil
})
}
func WithAPIURL(baseURL string) Option {
return optionFunc(func(c *Client) error {
if !strings.HasSuffix(baseURL, "/") {
baseURL += "/"
}
u, err := url.Parse(baseURL)
if err != nil {
return err
}
c.APIURL = u
return nil
})
}
func WithHTTPClient(httpClient *http.Client) Option {
return optionFunc(func(c *Client) error {
c.HTTPClient = httpClient
return nil
})
}
func WithUserAgent(userAgent string) Option {
return optionFunc(func(c *Client) error {
c.UserAgent = userAgent
return nil
})
}
func WithLogger(logger zerolog.Logger) Option {
return optionFunc(func(c *Client) error {
c.Logger = logger
return nil
})
}
type HTTPClient interface {
Do(req *http.Request) (*http.Response, error)
}
type Client struct {
HTTPClient HTTPClient
APIURL *url.URL
AuthToken string
UserAgent string
Logger zerolog.Logger
}
func New(options ...Option) (*Client, error) {
c := &Client{
HTTPClient: http.DefaultClient,
APIURL: &DefaultAPIURL,
UserAgent: DefaultUserAgent,
Logger: zerolog.Nop(),
}
err := c.Set(options...)
return c, err
}
func (c *Client) Set(options ...Option) error {
for _, opt := range options {
err := opt.apply(c)
if err != nil {
return err
}
}
return nil
}
func (c *Client) Do(req *http.Request) (*http.Response, error) {
req.URL = c.APIURL.ResolveReference(req.URL)
c.Logger.Debug().Str("url", req.URL.String()).Msg("request")
req.Header.Set("Accept", "application/json")
if c.AuthToken != "" {
req.Header.Set(
"Cookie", "__Secure-next-auth.session-token="+c.AuthToken,
)
}
if c.UserAgent != "" {
req.Header.Set("User-Agent", c.UserAgent)
}
return c.HTTPClient.Do(req)
}

57
midjourney/job.go Normal file
View File

@@ -0,0 +1,57 @@
package midjourney
type JobType string
const (
JobTypeGrid JobType = "grid"
JobTypeUpscale JobType = "upscale"
)
type JobStatus string
const (
JobStatusRunning JobStatus = "running"
JobStatusCompleted JobStatus = "completed"
)
type Event struct {
Height int `json:"height,omitempty"`
TextPrompt []string `json:"textPrompt,omitempty"`
ImagePrompts []string `json:"imagePrompts,omitempty"`
Width int `json:"width,omitempty"`
BatchSize int `json:"batchSize,omitempty"`
SeedImageURL string `json:"seedImageURL,omitempty"`
}
type Job struct {
CurrentStatus JobStatus `json:"current_status,omitempty"`
EnqueueTime string `json:"enqueue_time,omitempty"`
Event *Event `json:"event,omitempty"`
Flagged bool `json:"flagged,omitempty"`
FollowedByUser bool `json:"followed_by_user,omitempty"`
GridID string `json:"grid_id,omitempty"`
GridNum string `json:"grid_num,omitempty"`
GuildID string `json:"guild_id,omitempty"`
Hidden bool `json:"hidden,omitempty"`
ID string `json:"id,omitempty"`
ImagePaths []string `json:"image_paths,omitempty"`
IsPublished bool `json:"is_published,omitempty"`
LikedByUser bool `json:"liked_by_user,omitempty"`
LowPriority bool `json:"low_priority,omitempty"`
Metered bool `json:"metered,omitempty"`
ModHidden bool `json:"mod_hidden,omitempty"`
Platform string `json:"platform,omitempty"`
PlatformChannel string `json:"platform_channel,omitempty"`
PlatformChannelID string `json:"platform_channel_id,omitempty"`
PlatformMessageID string `json:"platform_message_id,omitempty"`
PlatformThreadID string `json:"platform_thread_id,omitempty"`
Prompt string `json:"prompt,omitempty"`
RankedByUser bool `json:"ranked_by_user,omitempty"`
RankingByUser int `json:"ranking_by_user,omitempty"`
Type JobType `json:"type,omitempty"`
UserID string `json:"user_id,omitempty"`
Username string `json:"username,omitempty"`
FullCommand string `json:"full_command,omitempty"`
ReferenceJobID string `json:"reference_job_id,omitempty"`
ReferenceImageNum string `json:"reference_image_num,omitempty"`
}

4
midjourney/midjourney.go Normal file
View File

@@ -0,0 +1,4 @@
// Package midjourney provides a basic read-only API client for MidJourney. As
// there is no official API, it uses the same API as the MidJourney website
// uses.
package midjourney

124
midjourney/recent_jobs.go Normal file
View File

@@ -0,0 +1,124 @@
package midjourney
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"time"
)
const FromDateFormat = "2006-01-02 15:04:05.999999"
type RecentJobsQuery struct {
Amount int
JobType JobType
OrderBy Order
JobStatus JobStatus
UserID string
FromDate time.Time
Page int
Dedupe bool
RefreshAPI int
}
func (rjq *RecentJobsQuery) Values() url.Values {
v := url.Values{}
if rjq.Amount != 0 {
v.Set("amount", strconv.Itoa(rjq.Amount))
}
if rjq.JobType != "" {
v.Set("jobType", string(rjq.JobType))
}
if rjq.OrderBy != "" {
v.Set("orderBy", string(rjq.OrderBy))
}
if rjq.JobStatus != "" {
v.Set("jobStatus", string(rjq.JobStatus))
}
if rjq.UserID != "" {
v.Set("userId", rjq.UserID)
}
if !rjq.FromDate.IsZero() {
v.Set("fromDate", rjq.FromDate.Format(FromDateFormat))
}
if rjq.Page != 0 {
v.Set("page", strconv.Itoa(rjq.Page))
}
if rjq.Dedupe {
v.Set("dedupe", "true")
}
if rjq.RefreshAPI != 0 {
v.Set("refreshApi", strconv.Itoa(rjq.RefreshAPI))
}
return v
}
func (rjq *RecentJobsQuery) NextPage() *RecentJobsQuery {
q := *rjq
if q.OrderBy == OrderNew && q.FromDate.IsZero() {
q.FromDate = time.Now().UTC()
}
if q.Page == 0 {
q.Page = 1
}
q.Page = rjq.Page + 1
return &q
}
type RecentJobs struct {
Query RecentJobsQuery
Jobs []*Job
Page int
}
func (c *Client) RecentJobs(
ctx context.Context,
q *RecentJobsQuery,
) (*RecentJobs, error) {
u := &url.URL{
Path: "app/recent-jobs",
RawQuery: q.Values().Encode(),
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
fromDate := q.FromDate
if fromDate.IsZero() {
fromDate = time.Now().UTC()
}
resp, err := c.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %s", ErrResponseStatus, resp.Status)
}
rj := &RecentJobs{
Query: *q,
Jobs: []*Job{},
Page: q.Page,
}
err = json.NewDecoder(resp.Body).Decode(&rj.Jobs)
if err != nil {
return nil, err
}
if rj.Query.OrderBy == OrderNew && rj.Query.FromDate.IsZero() {
rj.Query.FromDate = fromDate
}
return rj, nil
}