Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
byte-bandit committed Nov 9, 2021
1 parent 8b2128e commit 6291292
Show file tree
Hide file tree
Showing 7 changed files with 320 additions and 0 deletions.
6 changes: 6 additions & 0 deletions .env.local
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export IFTTT_EVENT_NAME=""
export IFTTT_KEY=""
export REDDIT_APP_ID=""
export REDDIT_APP_SECRET=""
export REDDIT_USERNAME=""
export REDDIT_PASSWORD=""
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.env
bin/*
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.PHONY: build
build:
@GOOS=linux GOARCH=amd64 go build -ldflags='-X main.version=1.0.0's -o bin/turnipmon .
17 changes: 17 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# TurnipMon - Animal Crossing Turnip Market Monitor

This hastily slapped together project can easily be run on a free t2micro instance or any other cheap server infrastructure you might have available. It will constantly monitor two popular turnip markets on reddit for new available trades and send an update to your phone within 90 seconds using IFTTT.

The idea for this project initially stemmed from finding the official reddit app insufficient for my needs of quick-ish notifications for certain subreddits only.

This was written up and tested over the duration of a coffee infused early morning, there are no tests and code quality is lacking severely. Still, if you're interested in giving it a try, please go ahead. I might come back and do all the cleanup and housekeeping work needed to make this solution suitable for a broader audience in the future, given the interest.

# How to run

- Setup your IFTTT project, using a web hook (without JSON payload) as trigger and a notification as action. The IFTTT app will need to be installed on your phone for the notification to be received on there
- Log in to reddit and follow the steps outlined [here](https://github.com/reddit-archive/reddit/wiki/OAuth2-Quick-Start-Example#first-steps) in order to create your own app. This step is needed to due to inconsistent rate limiting on the public API access.
- Run `make build` in order to compile for x64 Linux or adapt the file as suited for your needs
- Take a look at the provided `.env.local` or call `turnipmon --help` to see how to supply the necessary configuration values
- Run the binary
- ???
- Profit!
12 changes: 12 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module github.com/klausklapper/turnipmon

go 1.16

require (
github.com/go-resty/resty/v2 v2.7.0
github.com/integrii/flaggy v1.4.4
github.com/madflojo/tasks v1.0.0
github.com/pterm/pterm v0.12.33
github.com/vartanbeno/go-reddit/v2 v2.0.1
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4
)
75 changes: 75 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
github.com/MarvinJWendt/testza v0.2.10 h1:cX4zE9TofXxe72a6EPIYAxC+8cVWTsmmgsXTZIT+5bQ=
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU=
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-resty/resty/v2 v2.7.0 h1:me+K9p3uhSmXtrBZ4k9jcEAfJmuC8IivWHwaLZwPrFY=
github.com/go-resty/resty/v2 v2.7.0/go.mod h1:9PWDzw47qPphMRFfhsyk0NnSgvluHcljSMVIq3w7q0I=
github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk=
github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck=
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
github.com/integrii/flaggy v1.4.4 h1:8fGyiC14o0kxhTqm2VBoN19fDKPZsKipP7yggreTMDc=
github.com/integrii/flaggy v1.4.4/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI=
github.com/madflojo/tasks v1.0.0 h1:BSAA8Ajs+V/9IuALWXTWOITcmaVWRwvANLW701HaI+g=
github.com/madflojo/tasks v1.0.0/go.mod h1:kZDvlF5pOIiMnvS0qzfUOTcZSNsJ79oRRe49HrKAXvU=
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
github.com/pterm/pterm v0.12.33 h1:XiT50Pvdqn5O8FAiIqZMpXP6NkVEcmlUa+mkA1yWVCg=
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rs/xid v1.2.1 h1:mhH9Nq+C1fY2l1XIpgxIiUOfNpRBYH1kKcr+qfKgjRc=
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vartanbeno/go-reddit/v2 v2.0.1 h1:P6ITpf5YHjdy7DHZIbUIDn/iNAoGcEoDQnMa+L4vutw=
github.com/vartanbeno/go-reddit/v2 v2.0.1/go.mod h1:758/S10hwZSLm43NPtwoNQdZFSg3sjB5745Mwjb0ANI=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb h1:pirldcYWx7rx7kE5r+9WsOXPXK0+WH5+uZ7uPmJ44uM=
golang.org/x/net v0.0.0-20211029224645-99673261e6eb/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw=
golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8=
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
205 changes: 205 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
package main

import (
"context"
"errors"
"fmt"
"os"
"os/signal"
"syscall"
"time"

"github.com/go-resty/resty/v2"
"github.com/integrii/flaggy"
"github.com/madflojo/tasks"
"github.com/pterm/pterm"
"github.com/vartanbeno/go-reddit/v2/reddit"
)

var (
cfgIftttEventName string = os.Getenv("IFTTT_EVENT_NAME")
cfgIftttKey string = os.Getenv("IFTTT_KEY")
cfgRedditAppID string = os.Getenv("REDDIT_APP_ID")
cfgRedditAppSecret string = os.Getenv("REDDIT_APP_SECRET")
cfgRedditUsername string = os.Getenv("REDDIT_USERNAME")
cfgRedditPassword string = os.Getenv("REDDIT_PASSWORD")
lastPost *reddit.Post
sprintIDStyle = pterm.NewStyle(pterm.FgGray, pterm.BgLightCyan)
sprintTitleStyle = pterm.NewStyle(pterm.FgGray, pterm.BgLightMagenta)
version string = "unknown"
)

func main() {
mainScreen()
flaggy.SetName("TurnipMon")
flaggy.SetDescription("A little Animal Crossing NH turnip marketplace subreddit monitor, sending you phone notifications via IFTTT once a new turnip trade has been opened.")
flaggy.DefaultParser.ShowHelpOnUnexpected = true
flaggy.DefaultParser.AdditionalHelpPrepend = "http://github.com/klausklapper/turnipmon"
flaggy.String(&cfgIftttEventName, "n", "name", "[env: IFTTT_EVENT_NAME] Required. The IFTTT web hook event name to trigger.")
flaggy.String(&cfgIftttKey, "k", "key", "[env: IFTTT_KEY] Required. Your IFTTT web hook key (see https://ifttt.com/maker_webhooks).")
flaggy.String(&cfgRedditAppID, "i", "id", "[env: REDDIT_APP_ID] Required. Your Reddit app API ID credential.")
flaggy.String(&cfgRedditAppSecret, "s", "secret", "[env: REDDIT_APP_SECRET] Required. Your Reddit app API secret.")
flaggy.String(&cfgRedditUsername, "u", "username", "[env: REDDIT_USERNAME] Required. Your Reddit username.")
flaggy.String(&cfgRedditPassword, "p", "password", "[env: REDDIT_PASSWORD] Required. Your Reddit password.")
flaggy.SetVersion(version)
flaggy.Parse()

if len(cfgIftttEventName) < 1 {
pterm.Fatal.Println("Missing IFTTT web hook event name. Use the --help argument for more information.")
}
if len(cfgIftttKey) < 1 {
pterm.Fatal.Println("Missing IFTTT web hook key. Use the --help argument for more information.")
}
if len(cfgRedditAppID) < 1 {
pterm.Fatal.Println("Missing Reddit app ID. Use the --help argument for more information.")
}
if len(cfgRedditAppSecret) < 1 {
pterm.Fatal.Println("Missing Reddit app secret. Use the --help argument for more information.")
}
if len(cfgRedditUsername) < 1 {
pterm.Fatal.Println("Missing Reddit username. Use the --help argument for more information.")
}
if len(cfgRedditPassword) < 1 {
pterm.Fatal.Println("Missing Reddit password. Use the --help argument for more information.")
}

pterm.Success.Printfln("Configuration parsed! Event: %s, Key: [REDACTED]", cfgIftttEventName)

ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(quit)

pterm.Info.Println("Connecting to Reddit ...")
credentials := reddit.Credentials{ID: cfgRedditAppID, Secret: cfgRedditAppSecret, Username: cfgRedditUsername, Password: cfgRedditPassword}
client, err := reddit.NewClient(credentials)
pterm.Fatal.PrintOnError(err)

pterm.Info.Println("Fast forwarding to latest posts ...")
posts, _, err := client.Subreddit.NewPosts(ctx, "acturnips+ACNHTurnips", &reddit.ListOptions{
Limit: 5,
})
pterm.Fatal.PrintOnError(err)
if len(posts) < 1 {
pterm.Fatal.Println("No posts found. This should never happen?")
}

updateLastPost(posts[0])
pterm.Info.Printfln("Fast forwarded to %v%v", sprintID(lastPost.FullID), sprintTitle(lastPost.Title))

if time.Now().UTC().Sub(lastPost.Created.UTC()) < time.Minute*30 {
pterm.Success.Printfln("🎉 Last post %v created within the last 30 minutes. Notifying your phone ... 🌈 🥕 ", sprintID(lastPost.FullID))
err = sendIftttRequest(ctx, lastPost.Title, lastPost.URL)
pterm.Fatal.PrintOnError(err)
} else {
pterm.Warning.Printfln("Last post %v is older than 30 minutes. Assuming it's already expired, no phone notification will be sent.", sprintID(lastPost.FullID))
}
pterm.Info.Println("All caught up.")
spinner := newSpinner()

scheduler := tasks.New()
defer scheduler.Stop()
_, err = scheduler.Add(&tasks.Task{
Interval: time.Duration(90 * time.Second),
ErrFunc: func(err error) {
pterm.Fatal.Println(err)
},
TaskFunc: func() error {
spinner.UpdateText("Checking ...")

posts, _, err := client.Subreddit.NewPosts(ctx, "acturnips+ACNHTurnips", &reddit.ListOptions{
Limit: 5,
Before: lastPost.FullID,
})
if err != nil {
return err
}
if len(posts) < 1 {
spinner.UpdateText("No new trades found. Next check in 90 seconds")
} else {
spinner.Success(fmt.Sprintf("Found %d new trades!", len(posts)))
for _, v := range posts {
pterm.Success.Printfln("🎉 %v%v created within the last 30 minutes. Notifying your phone ... 🌈 🥕 ", sprintID(v.FullID), sprintTitle(v.Title))
err = sendIftttRequest(ctx, v.Title, v.URL)
if err != nil {
return err
}
}
updateLastPost(posts[0])
spinner = newSpinner()
}
return nil
},
})
pterm.Fatal.PrintOnError(err)

select {
case <-quit:
break
case <-ctx.Done():
break
}

pterm.Info.Println("Shutdown requested ...")
scheduler.Stop()
cancel()

pterm.Success.Println("Goodbye! 👋")

}

func newSpinner() *pterm.SpinnerPrinter {
spinner, err := pterm.DefaultSpinner.Start()
pterm.Fatal.PrintOnError(err)
spinner.UpdateText("No new trades found. Next check in 90 seconds")
return spinner
}

func updateLastPost(p *reddit.Post) {
lastPost = p
}

func sprintID(id string) string {
return sprintIDStyle.Sprintf("[%v]", id)
}

func sprintTitle(title string) string {
return sprintTitleStyle.Sprint(title)
}

func mainScreen() {
print("\033[H\033[2J")
ptermLogo, _ := pterm.DefaultBigText.WithLetters(
pterm.NewLettersFromStringWithStyle("Turnip", pterm.NewStyle(pterm.FgLightCyan)),
pterm.NewLettersFromStringWithStyle("Mon", pterm.NewStyle(pterm.FgLightMagenta))).
Srender()

pterm.DefaultCenter.Print(ptermLogo)
pterm.DefaultCenter.Print(pterm.DefaultHeader.WithFullWidth().WithBackgroundStyle(pterm.NewStyle(pterm.BgLightBlue)).WithMargin(10).Sprint("🥕 TurnipMon - Animal Crossing New Horizons Turnip Market Monitor"))
}

func sendIftttRequest(ctx context.Context, title string, uri string) error {
type payload struct {
Value1 string `json:"value1"`
Value2 string `json:"value2"`
Value3 string `json:"value3"`
}

resp, err := resty.New().R().
SetContext(ctx).
SetBody(payload{Value1: title, Value2: uri, Value3: "https://dodo.ac/np/images/8/86/Turnips_NH_Inv_Icon.png"}).
Post(fmt.Sprintf("https://maker.ifttt.com/trigger/%s/with/key/%s", cfgIftttEventName, cfgIftttKey))

if err != nil {
return err
}
if !resp.IsSuccess() {
pterm.Error.Printfln("IFTTT Request failed with [%v] %v: %v", resp.StatusCode(), resp.Status(), resp.Result())
return errors.New("failed to send IFTTT request")
}

return nil
}

0 comments on commit 6291292

Please sign in to comment.