mirror of
https://github.com/christianselig/apollo-backend
synced 2024-11-25 13:17:42 +00:00
initial pass
This commit is contained in:
commit
7fad5ac9e4
30 changed files with 1033 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
tmp/
|
||||||
|
.env
|
9
Brewfile
Normal file
9
Brewfile
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
tap 'contribsys/faktory'
|
||||||
|
|
||||||
|
brew 'faktory'
|
||||||
|
brew 'foreman'
|
||||||
|
brew 'golang'
|
||||||
|
brew 'golang-migrate'
|
||||||
|
brew 'postgres'
|
||||||
|
brew 'redis'
|
||||||
|
|
177
Brewfile.lock.json
Normal file
177
Brewfile.lock.json
Normal file
|
@ -0,0 +1,177 @@
|
||||||
|
{
|
||||||
|
"entries": {
|
||||||
|
"brew": {
|
||||||
|
"postgres": {
|
||||||
|
"version": "13.2_2",
|
||||||
|
"bottle": {
|
||||||
|
"rebuild": 0,
|
||||||
|
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||||
|
"files": {
|
||||||
|
"arm64_big_sur": {
|
||||||
|
"cellar": "/opt/homebrew/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:5bb80b319443cc57a44a9a9a8507a5255728d2caf359ef7e931c79c2c833b900",
|
||||||
|
"sha256": "5bb80b319443cc57a44a9a9a8507a5255728d2caf359ef7e931c79c2c833b900"
|
||||||
|
},
|
||||||
|
"big_sur": {
|
||||||
|
"cellar": "/usr/local/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:ee460f1f8beb4d877121faf4ebcbf8069f86a8ef4ae05fa342bc2fcdde75bb47",
|
||||||
|
"sha256": "ee460f1f8beb4d877121faf4ebcbf8069f86a8ef4ae05fa342bc2fcdde75bb47"
|
||||||
|
},
|
||||||
|
"catalina": {
|
||||||
|
"cellar": "/usr/local/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:1f50565ab7a703d85ecc37179e8d461dc82de4e3e6266ba085dd19a0b95ed5db",
|
||||||
|
"sha256": "1f50565ab7a703d85ecc37179e8d461dc82de4e3e6266ba085dd19a0b95ed5db"
|
||||||
|
},
|
||||||
|
"mojave": {
|
||||||
|
"cellar": "/usr/local/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/postgresql/blobs/sha256:c0a5828cb5caef09e7b4acd99b450b6d4ceb3a0d265a95b147e63e23fb6f7596",
|
||||||
|
"sha256": "c0a5828cb5caef09e7b4acd99b450b6d4ceb3a0d265a95b147e63e23fb6f7596"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"golang": {
|
||||||
|
"version": "1.16.3",
|
||||||
|
"bottle": {
|
||||||
|
"rebuild": 0,
|
||||||
|
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||||
|
"files": {
|
||||||
|
"arm64_big_sur": {
|
||||||
|
"cellar": "/opt/homebrew/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:e7c1efdd09e951eb46d01a3200b01e7fa55ce285b75470051be7fef34f4233ce",
|
||||||
|
"sha256": "e7c1efdd09e951eb46d01a3200b01e7fa55ce285b75470051be7fef34f4233ce"
|
||||||
|
},
|
||||||
|
"big_sur": {
|
||||||
|
"cellar": "/usr/local/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:ea37f33fd27369612a3e4e6db6adc46db0e8bdf6fac1332bf51bafaa66d43969",
|
||||||
|
"sha256": "ea37f33fd27369612a3e4e6db6adc46db0e8bdf6fac1332bf51bafaa66d43969"
|
||||||
|
},
|
||||||
|
"catalina": {
|
||||||
|
"cellar": "/usr/local/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:69c28f5e60612801c66e51e93d32068f822b245ab83246cb6cb374572eb59e15",
|
||||||
|
"sha256": "69c28f5e60612801c66e51e93d32068f822b245ab83246cb6cb374572eb59e15"
|
||||||
|
},
|
||||||
|
"mojave": {
|
||||||
|
"cellar": "/usr/local/Cellar",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/go/blobs/sha256:bf1e90ed1680b8ee1acb49f2f99426c8a8ac3e49efd63c7f3b41e57e7214dd19",
|
||||||
|
"sha256": "bf1e90ed1680b8ee1acb49f2f99426c8a8ac3e49efd63c7f3b41e57e7214dd19"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreman": {
|
||||||
|
"version": "0.87.2",
|
||||||
|
"bottle": {
|
||||||
|
"rebuild": 0,
|
||||||
|
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||||
|
"files": {
|
||||||
|
"arm64_big_sur": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:575f9fbc16eca16cf479196ce44d87bb817ddb1e2eed59869ffe158d98d08a9f",
|
||||||
|
"sha256": "575f9fbc16eca16cf479196ce44d87bb817ddb1e2eed59869ffe158d98d08a9f"
|
||||||
|
},
|
||||||
|
"big_sur": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:70c762dd642d8f5aa3ca5a28e420b6c9f7befaf7699de073b7d62e174fdee88f",
|
||||||
|
"sha256": "70c762dd642d8f5aa3ca5a28e420b6c9f7befaf7699de073b7d62e174fdee88f"
|
||||||
|
},
|
||||||
|
"catalina": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:5c2b39c1f7e9667b9ebc6b7228b6cf31f06c2261c85019028272cfdda7073ea5",
|
||||||
|
"sha256": "5c2b39c1f7e9667b9ebc6b7228b6cf31f06c2261c85019028272cfdda7073ea5"
|
||||||
|
},
|
||||||
|
"mojave": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:674b5fc005986f47294acedccba6b2a2bcdc1d423e392a356f8d58cc88a2c81a",
|
||||||
|
"sha256": "674b5fc005986f47294acedccba6b2a2bcdc1d423e392a356f8d58cc88a2c81a"
|
||||||
|
},
|
||||||
|
"high_sierra": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/foreman/blobs/sha256:b0d289ff31caf33f3d549af6dd615e37588aadb243355395380c4df5b0e52d63",
|
||||||
|
"sha256": "b0d289ff31caf33f3d549af6dd615e37588aadb243355395380c4df5b0e52d63"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"faktory": {
|
||||||
|
"version": "1.5.1-1",
|
||||||
|
"bottle": false
|
||||||
|
},
|
||||||
|
"redis": {
|
||||||
|
"version": "6.2.3",
|
||||||
|
"bottle": {
|
||||||
|
"rebuild": 0,
|
||||||
|
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||||
|
"files": {
|
||||||
|
"arm64_big_sur": {
|
||||||
|
"cellar": ":any",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:b2b3cfeca2d5f110507e9e7a7af8918786f2853e39e49b0b39de68762e5b5030",
|
||||||
|
"sha256": "b2b3cfeca2d5f110507e9e7a7af8918786f2853e39e49b0b39de68762e5b5030"
|
||||||
|
},
|
||||||
|
"big_sur": {
|
||||||
|
"cellar": ":any",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:d891c5b376746c3895098fd384fb4edba972b532848f63cbad5be20e611458ac",
|
||||||
|
"sha256": "d891c5b376746c3895098fd384fb4edba972b532848f63cbad5be20e611458ac"
|
||||||
|
},
|
||||||
|
"catalina": {
|
||||||
|
"cellar": ":any",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:a269e87b26515775a7034d9d6cb996ed63d783b5a6d681b64bab92ce93bed55b",
|
||||||
|
"sha256": "a269e87b26515775a7034d9d6cb996ed63d783b5a6d681b64bab92ce93bed55b"
|
||||||
|
},
|
||||||
|
"mojave": {
|
||||||
|
"cellar": ":any",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/redis/blobs/sha256:3373d834552eef5f6c71889299124693de6b5d5b887e520d6db96ab51da81020",
|
||||||
|
"sha256": "3373d834552eef5f6c71889299124693de6b5d5b887e520d6db96ab51da81020"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"golang-migrate": {
|
||||||
|
"version": "4.14.1",
|
||||||
|
"bottle": {
|
||||||
|
"rebuild": 0,
|
||||||
|
"root_url": "https://ghcr.io/v2/homebrew/core",
|
||||||
|
"files": {
|
||||||
|
"arm64_big_sur": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:3565f7a03cfd1eeec3110aa8d56f03baa79b0de2718103c0095e51187ecd37ee",
|
||||||
|
"sha256": "3565f7a03cfd1eeec3110aa8d56f03baa79b0de2718103c0095e51187ecd37ee"
|
||||||
|
},
|
||||||
|
"big_sur": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:5c61a106d9970b0f9b14e78e1523894d57b50cd0473f7d5a1fb1a9161dbff159",
|
||||||
|
"sha256": "5c61a106d9970b0f9b14e78e1523894d57b50cd0473f7d5a1fb1a9161dbff159"
|
||||||
|
},
|
||||||
|
"catalina": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:a77af5282af35e0d073e82140b091eedf0b478c19aea36f1b06738690989cebb",
|
||||||
|
"sha256": "a77af5282af35e0d073e82140b091eedf0b478c19aea36f1b06738690989cebb"
|
||||||
|
},
|
||||||
|
"mojave": {
|
||||||
|
"cellar": ":any_skip_relocation",
|
||||||
|
"url": "https://ghcr.io/v2/homebrew/core/golang-migrate/blobs/sha256:8fa3758e979f09c171388887c831a6518e3f8df67b07668b6c8cebf76b19a653",
|
||||||
|
"sha256": "8fa3758e979f09c171388887c831a6518e3f8df67b07668b6c8cebf76b19a653"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"tap": {
|
||||||
|
"contribsys/faktory": {
|
||||||
|
"revision": "ada2f8b18fe79a40a906a7371d36c1d750422ca3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"system": {
|
||||||
|
"macos": {
|
||||||
|
"big_sur": {
|
||||||
|
"HOMEBREW_VERSION": "3.1.5",
|
||||||
|
"HOMEBREW_PREFIX": "/usr/local",
|
||||||
|
"Homebrew/homebrew-core": "17fb25009a4876e9369cb621e798b758eb0c7fe6",
|
||||||
|
"CLT": "12.5.0.0.1.1617976050",
|
||||||
|
"Xcode": "12.5",
|
||||||
|
"macOS": "11.3.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
4
Procfile
Normal file
4
Procfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
postgres: postgres -D tmp/postgresql
|
||||||
|
faktory: faktory
|
||||||
|
server: go run github.com/andremedeiros/apollo/cmd/apollo-api
|
||||||
|
worker: go run github.com/andremedeiros/apollo/cmd/apollo-worker
|
14
README.md
Normal file
14
README.md
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
# Apollo API
|
||||||
|
|
||||||
|
## Getting started
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ script/bootstrap
|
||||||
|
$ cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the server
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ script/server
|
||||||
|
```
|
62
cmd/apollo-api/accounts.go
Normal file
62
cmd/apollo-api/accounts.go
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
|
"github.com/andremedeiros/apollo/internal/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *application) upsertAccountHandler(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
|
||||||
|
a := &data.Account{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(a); err != nil {
|
||||||
|
fmt.Println("failing on decoding json")
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
a.ExpiresAt = time.Now().Unix() + 3300
|
||||||
|
|
||||||
|
// Here we check whether the account is supplied with a valid token.
|
||||||
|
ac := app.client.NewAuthenticatedClient(a.RefreshToken, a.AccessToken)
|
||||||
|
me, err := ac.Me()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failing on fetching remote user")
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if me.NormalizedUsername() != a.NormalizedUsername() {
|
||||||
|
fmt.Println("failing on account username comparison")
|
||||||
|
app.errorResponse(w, r, 500, "nice try")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upsert account
|
||||||
|
if err := app.models.Accounts.Upsert(a); err != nil {
|
||||||
|
fmt.Println("failing on account upsert")
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Associate
|
||||||
|
d, err := app.models.Devices.GetByAPNSToken(ps.ByName("apns"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("failing on apns")
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.models.DevicesAccounts.Associate(a.ID, d.ID); err != nil {
|
||||||
|
fmt.Println("failing on associate")
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
25
cmd/apollo-api/devices.go
Normal file
25
cmd/apollo-api/devices.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
|
||||||
|
"github.com/andremedeiros/apollo/internal/data"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *application) upsertDeviceHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
d := &data.Device{}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(d); err != nil {
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.models.Devices.Upsert(d); err != nil {
|
||||||
|
app.errorResponse(w, r, 500, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
}
|
8
cmd/apollo-api/errors.go
Normal file
8
cmd/apollo-api/errors.go
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
|
func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message string) {
|
||||||
|
w.WriteHeader(status)
|
||||||
|
w.Write([]byte(message))
|
||||||
|
}
|
18
cmd/apollo-api/health.go
Normal file
18
cmd/apollo-api/health.go
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *application) healthCheckHandler(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
||||||
|
data := map[string]string{
|
||||||
|
"status": "available",
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusOK)
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
json.NewEncoder(w).Encode(data)
|
||||||
|
}
|
81
cmd/apollo-api/main.go
Normal file
81
cmd/apollo-api/main.go
Normal file
|
@ -0,0 +1,81 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/andremedeiros/apollo/internal/data"
|
||||||
|
"github.com/andremedeiros/apollo/internal/reddit"
|
||||||
|
|
||||||
|
faktory "github.com/contribsys/faktory/client"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
_ "github.com/lib/pq"
|
||||||
|
)
|
||||||
|
|
||||||
|
type config struct {
|
||||||
|
port int
|
||||||
|
}
|
||||||
|
|
||||||
|
type application struct {
|
||||||
|
cfg config
|
||||||
|
logger *log.Logger
|
||||||
|
db *sql.DB
|
||||||
|
faktory *faktory.Client
|
||||||
|
models *data.Models
|
||||||
|
client *reddit.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg config
|
||||||
|
flag.IntVar(&cfg.port, "port", 4000, "API server port")
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
faktory, err := faktory.Open()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer faktory.Close()
|
||||||
|
|
||||||
|
rc := reddit.NewClient(os.Getenv("REDDIT_CLIENT_ID"), os.Getenv("REDDIT_CLIENT_SECRET"))
|
||||||
|
|
||||||
|
app := &application{
|
||||||
|
cfg,
|
||||||
|
logger,
|
||||||
|
db,
|
||||||
|
faktory,
|
||||||
|
data.NewModels(db),
|
||||||
|
rc,
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: fmt.Sprintf(":%d", cfg.port),
|
||||||
|
Handler: app.routes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Printf("starting server on %s", srv.Addr)
|
||||||
|
err = srv.ListenAndServe()
|
||||||
|
logger.Fatal(err)
|
||||||
|
|
||||||
|
/*
|
||||||
|
rc := reddit.NewClient("C7MjYkx1czyRDA", "I2AsVWbrf8h4vdQxVa5Pvf84vScF1w")
|
||||||
|
rac := rc.NewAuthenticatedClient("2532458-kGp6OeR-LMoQNrUXRL-7UNfyBbViRA", "2532458-UE7IvJK3-VuTdMJB0bgMv58fxKQhww")
|
||||||
|
rac.MessageInbox()
|
||||||
|
*/
|
||||||
|
}
|
16
cmd/apollo-api/routes.go
Normal file
16
cmd/apollo-api/routes.go
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/julienschmidt/httprouter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (app *application) routes() *httprouter.Router {
|
||||||
|
router := httprouter.New()
|
||||||
|
|
||||||
|
router.GET("/v1/health", app.healthCheckHandler)
|
||||||
|
|
||||||
|
router.POST("/v1/device", app.upsertDeviceHandler)
|
||||||
|
router.POST("/v1/device/:apns/account", app.upsertAccountHandler)
|
||||||
|
|
||||||
|
return router
|
||||||
|
}
|
49
cmd/apollo-worker/main.go
Normal file
49
cmd/apollo-worker/main.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
worker "github.com/contribsys/faktory_worker_go"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
|
||||||
|
"github.com/andremedeiros/apollo/internal/data"
|
||||||
|
"github.com/andremedeiros/apollo/internal/reddit"
|
||||||
|
)
|
||||||
|
|
||||||
|
type application struct {
|
||||||
|
logger *log.Logger
|
||||||
|
db *sql.DB
|
||||||
|
models *data.Models
|
||||||
|
client *reddit.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
err := godotenv.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger := log.New(os.Stdout, "", log.Ldate|log.Ltime)
|
||||||
|
|
||||||
|
db, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
rc := reddit.NewClient(os.Getenv("REDDIT_CLIENT_ID"), os.Getenv("REDDIT_CLIENT_SECRET"))
|
||||||
|
|
||||||
|
app := &application{
|
||||||
|
logger,
|
||||||
|
db,
|
||||||
|
data.NewModels(db),
|
||||||
|
rc,
|
||||||
|
}
|
||||||
|
|
||||||
|
mgr := worker.NewManager()
|
||||||
|
mgr.ProcessStrictPriorityQueues("critical", "default", "bulk")
|
||||||
|
mgr.Concurrency = 20
|
||||||
|
mgr.Run()
|
||||||
|
}
|
4
env.example
Normal file
4
env.example
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
DATABASE_URL=postgres://apollo:@localhost/apollo?sslmode=disable
|
||||||
|
FAKTORY_URL=tcp://localhost:7419
|
||||||
|
REDDIT_CLIENT_ID=C7MjYkx1czyRDA
|
||||||
|
REDDIT_CLIENT_SECRET=I2AsVWbrf8h4vdQxVa5Pvf84vScF1w
|
13
go.mod
Normal file
13
go.mod
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
module github.com/andremedeiros/apollo
|
||||||
|
|
||||||
|
go 1.16
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/contribsys/faktory v1.5.1
|
||||||
|
github.com/contribsys/faktory_worker_go v1.4.2
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/joho/godotenv v1.3.0
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0
|
||||||
|
github.com/lib/pq v1.10.1
|
||||||
|
github.com/stretchr/testify v1.5.1 // indirect
|
||||||
|
)
|
91
go.sum
Normal file
91
go.sum
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
|
github.com/benbjohnson/ego v0.4.0/go.mod h1:6Qzj43pKX58WdlHDZKZS4Q/XXYwz6toIqXqNjzt4mNA=
|
||||||
|
github.com/contribsys/faktory v1.3.0-1/go.mod h1:qCp3bcPrIT7mb/aR5KendsK/Nyg7yE3JOg4sETojQN8=
|
||||||
|
github.com/contribsys/faktory v1.5.1 h1:Ac22RjbR8USVEwRV7jvz0+ytp2NquL5j7HC0IRM1K34=
|
||||||
|
github.com/contribsys/faktory v1.5.1/go.mod h1:f0dpwzksA7TmHquOcU9/j1NUUz8YNdx8LYDoNFIU9gw=
|
||||||
|
github.com/contribsys/faktory_worker_go v1.4.2 h1:IJD4wGk6iZSOkLEOHX42s6R8YvnQI2gEzN6WeZO9BBg=
|
||||||
|
github.com/contribsys/faktory_worker_go v1.4.2/go.mod h1:v53dJh0lrk18B/3zFJjAWM6/Lgq341sRricsBUZ/umc=
|
||||||
|
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/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||||
|
github.com/go-bindata/go-bindata v3.1.2+incompatible/go.mod h1:xK8Dsgwmeed+BBsSy2XTopBn/8uK2HWuGSnA11C3Joo=
|
||||||
|
github.com/go-redis/redis v6.15.7+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||||
|
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||||
|
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||||
|
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||||
|
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||||
|
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||||
|
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
|
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||||
|
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
|
||||||
|
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||||
|
github.com/justinas/nosurf v1.1.0/go.mod h1:ALpWdSbuNGy2lZWtyXdjkYv4edL23oSEgfBT1gPJ5BQ=
|
||||||
|
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||||
|
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||||
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||||
|
github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg=
|
||||||
|
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||||
|
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||||
|
github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48=
|
||||||
|
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||||
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
|
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
|
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
|
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||||
|
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||||
|
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||||
|
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||||
|
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
|
||||||
|
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
51
internal/data/accounts.go
Normal file
51
internal/data/accounts.go
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Account struct {
|
||||||
|
ID int64
|
||||||
|
Username string
|
||||||
|
AccessToken string
|
||||||
|
RefreshToken string
|
||||||
|
ExpiresAt int64
|
||||||
|
LastMessageID string
|
||||||
|
LastCheckedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Account) NormalizedUsername() string {
|
||||||
|
return strings.ToLower(a.Username)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AccountModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountModel) Upsert(a *Account) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO accounts (username, access_token, refresh_token, expires_at, last_message_id, device_count, last_checked_at)
|
||||||
|
VALUES ($1, $2, $3, $4, '', 0, 0)
|
||||||
|
ON CONFLICT(username)
|
||||||
|
DO
|
||||||
|
UPDATE SET access_token = $2, refresh_token = $3, expires_at = $4, last_message_id = $5, last_checked_at = $6
|
||||||
|
RETURNING id`
|
||||||
|
|
||||||
|
args := []interface{}{a.NormalizedUsername(), a.AccessToken, a.RefreshToken, a.ExpiresAt, a.LastMessageID, a.LastCheckedAt}
|
||||||
|
return am.DB.QueryRow(query, args...).Scan(&a.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (am *AccountModel) Delete(id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockAccountModel struct{}
|
||||||
|
|
||||||
|
func (mam *MockAccountModel) Upsert(a *Account) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mam *MockAccountModel) Delete(id int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
42
internal/data/device_accounts.go
Normal file
42
internal/data/device_accounts.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import "database/sql"
|
||||||
|
|
||||||
|
type DeviceAccount struct {
|
||||||
|
ID int64
|
||||||
|
AccountID int64
|
||||||
|
DeviceID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceAccountModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dam *DeviceAccountModel) Associate(accountID int64, deviceID int64) error {
|
||||||
|
query := `
|
||||||
|
INSERT INTO devices_accounts (account_id, device_id)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (account_id, device_id) DO NOTHING
|
||||||
|
RETURNING id`
|
||||||
|
|
||||||
|
args := []interface{}{accountID, deviceID}
|
||||||
|
if err := dam.DB.QueryRow(query, args...).Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update account counter
|
||||||
|
query = `
|
||||||
|
UPDATE accounts
|
||||||
|
SET device_count = (
|
||||||
|
SELECT COUNT(*) FROM devices_accounts WHERE account_id = $1
|
||||||
|
)
|
||||||
|
WHERE id = $1`
|
||||||
|
args = []interface{}{accountID}
|
||||||
|
return dam.DB.QueryRow(query, args...).Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockDeviceAccountModel struct{}
|
||||||
|
|
||||||
|
func (mdam *MockDeviceAccountModel) Associate(accountID int64, deviceID int64) error {
|
||||||
|
return nil
|
||||||
|
}
|
67
internal/data/devices.go
Normal file
67
internal/data/devices.go
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Device struct {
|
||||||
|
ID int64
|
||||||
|
APNSToken string
|
||||||
|
Sandbox bool
|
||||||
|
LastPingedAt int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeviceModel struct {
|
||||||
|
DB *sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DeviceModel) Upsert(d *Device) error {
|
||||||
|
d.LastPingedAt = time.Now().Unix()
|
||||||
|
|
||||||
|
query := `
|
||||||
|
INSERT INTO devices (apns_token, sandbox, last_pinged_at)
|
||||||
|
VALUES ($1, $2, $3)
|
||||||
|
ON CONFLICT(apns_token)
|
||||||
|
DO
|
||||||
|
UPDATE SET last_pinged_at = $3
|
||||||
|
RETURNING id`
|
||||||
|
|
||||||
|
args := []interface{}{d.APNSToken, d.Sandbox, d.LastPingedAt}
|
||||||
|
return dm.DB.QueryRow(query, args...).Scan(&d.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dm *DeviceModel) GetByAPNSToken(token string) (*Device, error) {
|
||||||
|
query := `
|
||||||
|
SELECT id, apns_token, sandbox, last_pinged_at
|
||||||
|
FROM devices
|
||||||
|
WHERE apns_token = $1`
|
||||||
|
|
||||||
|
device := &Device{}
|
||||||
|
err := dm.DB.QueryRow(query, token).Scan(
|
||||||
|
&device.ID,
|
||||||
|
&device.APNSToken,
|
||||||
|
&device.Sandbox,
|
||||||
|
&device.LastPingedAt,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, sql.ErrNoRows):
|
||||||
|
return nil, ErrRecordNotFound
|
||||||
|
default:
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return device, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockDeviceModel struct{}
|
||||||
|
|
||||||
|
func (mdm *MockDeviceModel) Upsert(d *Device) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
func (mdm *MockDeviceModel) GetByAPNSToken(token string) (*Device, error) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
42
internal/data/models.go
Normal file
42
internal/data/models.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
package data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRecordNotFound = errors.New("record not found")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Models struct {
|
||||||
|
Accounts interface {
|
||||||
|
Upsert(a *Account) error
|
||||||
|
Delete(id int64) error
|
||||||
|
}
|
||||||
|
|
||||||
|
Devices interface {
|
||||||
|
Upsert(*Device) error
|
||||||
|
GetByAPNSToken(string) (*Device, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
DevicesAccounts interface {
|
||||||
|
Associate(int64, int64) error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewModels(db *sql.DB) *Models {
|
||||||
|
return &Models{
|
||||||
|
Accounts: &AccountModel{DB: db},
|
||||||
|
Devices: &DeviceModel{DB: db},
|
||||||
|
DevicesAccounts: &DeviceAccountModel{DB: db},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMockModels() *Models {
|
||||||
|
return &Models{
|
||||||
|
Accounts: &MockAccountModel{},
|
||||||
|
Devices: &MockDeviceModel{},
|
||||||
|
DevicesAccounts: &MockDeviceAccountModel{},
|
||||||
|
}
|
||||||
|
}
|
105
internal/reddit/client.go
Normal file
105
internal/reddit/client.go
Normal file
|
@ -0,0 +1,105 @@
|
||||||
|
package reddit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
tokenURL = "https://www.reddit.com/api/v1/access_token"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
id string
|
||||||
|
secret string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClient(id, secret string) *Client {
|
||||||
|
return &Client{id, secret}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthenticatedClient struct {
|
||||||
|
*Client
|
||||||
|
|
||||||
|
refreshToken string
|
||||||
|
accessToken string
|
||||||
|
expiry *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rc *Client) NewAuthenticatedClient(refreshToken, accessToken string) *AuthenticatedClient {
|
||||||
|
return &AuthenticatedClient{rc, refreshToken, accessToken, nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *AuthenticatedClient) request(r *Request) ([]byte, error) {
|
||||||
|
tr := &http.Transport{}
|
||||||
|
client := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
req, err := r.HTTPRequest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ioutil.ReadAll(resp.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *AuthenticatedClient) refreshTokens() (string, string, error) {
|
||||||
|
req := NewRequest(
|
||||||
|
WithMethod("POST"),
|
||||||
|
WithURL(tokenURL),
|
||||||
|
WithBody("grant_type", "refresh_token"),
|
||||||
|
WithBody("refresh_token", rac.refreshToken),
|
||||||
|
WithBasicAuth(rac.id, rac.secret),
|
||||||
|
)
|
||||||
|
|
||||||
|
body, err := rac.request(req)
|
||||||
|
fmt.Println(string(body))
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *AuthenticatedClient) MessageInbox() error {
|
||||||
|
req := NewRequest(
|
||||||
|
WithMethod("GET"),
|
||||||
|
WithToken(rac.accessToken),
|
||||||
|
WithURL("https://oauth.reddit.com/message/inbox.json"),
|
||||||
|
)
|
||||||
|
|
||||||
|
body, _ := rac.request(req)
|
||||||
|
fmt.Println(string(body))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MeResponse struct {
|
||||||
|
Name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (mr *MeResponse) NormalizedUsername() string {
|
||||||
|
return strings.ToLower(mr.Name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rac *AuthenticatedClient) Me() (*MeResponse, error) {
|
||||||
|
req := NewRequest(
|
||||||
|
WithMethod("GET"),
|
||||||
|
WithToken(rac.accessToken),
|
||||||
|
WithURL("https://oauth.reddit.com/api/v1/me"),
|
||||||
|
)
|
||||||
|
|
||||||
|
body, err := rac.request(req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
mr := &MeResponse{}
|
||||||
|
err = json.Unmarshal(body, mr)
|
||||||
|
|
||||||
|
return mr, err
|
||||||
|
}
|
76
internal/reddit/request.go
Normal file
76
internal/reddit/request.go
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
package reddit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const userAgent = "server:test-api:v0.0.1 (by /u/changelog)"
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
body url.Values
|
||||||
|
method string
|
||||||
|
token string
|
||||||
|
url string
|
||||||
|
auth string
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestOption func(*Request)
|
||||||
|
|
||||||
|
func NewRequest(opts ...RequestOption) *Request {
|
||||||
|
req := &Request{url.Values{}, "GET", "", "", ""}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
return req
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Request) HTTPRequest() (*http.Request, error) {
|
||||||
|
req, err := http.NewRequest(r.method, r.url, strings.NewReader(r.body.Encode()))
|
||||||
|
req.Header.Add("User-Agent", userAgent)
|
||||||
|
|
||||||
|
if r.token != "" {
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", r.token))
|
||||||
|
}
|
||||||
|
|
||||||
|
if r.auth != "" {
|
||||||
|
req.Header.Add("Authorization", fmt.Sprintf("Basic %s", r.auth))
|
||||||
|
}
|
||||||
|
|
||||||
|
return req, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithMethod(method string) RequestOption {
|
||||||
|
return func(req *Request) {
|
||||||
|
req.method = method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithURL(url string) RequestOption {
|
||||||
|
return func(req *Request) {
|
||||||
|
req.url = url
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBasicAuth(user, password string) RequestOption {
|
||||||
|
return func(req *Request) {
|
||||||
|
encoded := base64.StdEncoding.EncodeToString([]byte(user + ":" + password))
|
||||||
|
req.auth = encoded
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithToken(token string) RequestOption {
|
||||||
|
return func(req *Request) {
|
||||||
|
req.token = token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithBody(key, val string) RequestOption {
|
||||||
|
return func(req *Request) {
|
||||||
|
req.body.Set(key, val)
|
||||||
|
}
|
||||||
|
}
|
1
migrations/20210509150615_create_devices.down.sql
Normal file
1
migrations/20210509150615_create_devices.down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE devices;
|
8
migrations/20210509150615_create_devices.up.sql
Normal file
8
migrations/20210509150615_create_devices.up.sql
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS devices (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
apns_token character(100) UNIQUE,
|
||||||
|
sandbox boolean,
|
||||||
|
last_pinged_at integer
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS devices_pkey ON devices(id int4_ops);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS devices_apns_token_key ON devices(apns_token bpchar_ops);
|
1
migrations/20210509191431_create_accounts.down.sql
Normal file
1
migrations/20210509191431_create_accounts.down.sql
Normal file
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE accounts;
|
13
migrations/20210509191431_create_accounts.up.sql
Normal file
13
migrations/20210509191431_create_accounts.up.sql
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS accounts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
username character(20),
|
||||||
|
access_token character(64),
|
||||||
|
refresh_token character(64),
|
||||||
|
expires_at integer,
|
||||||
|
last_message_id character(32),
|
||||||
|
device_count integer,
|
||||||
|
last_checked_at integer
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS accounts_pkey ON accounts(id int4_ops);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS accounts_username_key ON accounts(username bpchar_ops);
|
7
migrations/20210509200230_create_devices_accounts.up.sql
Normal file
7
migrations/20210509200230_create_devices_accounts.up.sql
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
CREATE TABLE IF NOT EXISTS devices_accounts (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
account_id integer,
|
||||||
|
device_id integer
|
||||||
|
);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS devices_accounts_pkey ON devices_accounts(id int4_ops);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS devices_accounts_account_id_device_id_idx ON devices_accounts(account_id int4_ops,device_id int4_ops);
|
20
script/bootstrap
Executable file
20
script/bootstrap
Executable file
|
@ -0,0 +1,20 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
brew bundle check >/dev/null 2>&1 || {
|
||||||
|
echo "==> Installing Homebrew dependencies..."
|
||||||
|
brew bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
[ -d "tmp/postgresql" ] || {
|
||||||
|
echo "===> Setting up database..."
|
||||||
|
initdb -D tmp/postgresql -U apollo
|
||||||
|
}
|
||||||
|
|
||||||
|
go mod verify >/dev/null 2>&1 || {
|
||||||
|
echo "==> Installing Go dependencies..."
|
||||||
|
go mod download
|
||||||
|
}
|
16
script/migrate
Executable file
16
script/migrate
Executable file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# ensure everything in the app is up to date.
|
||||||
|
script/bootstrap
|
||||||
|
|
||||||
|
[ -f ".env" ] && {
|
||||||
|
source .env
|
||||||
|
|
||||||
|
echo "===> Running migrations..."
|
||||||
|
createdb -U apollo apollo || true
|
||||||
|
migrate -path=./migrations -database=$DATABASE_URL up
|
||||||
|
}
|
11
script/server
Executable file
11
script/server
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
# ensure everything in the app is up to date.
|
||||||
|
script/bootstrap
|
||||||
|
|
||||||
|
# boot the app and any other necessary processes.
|
||||||
|
foreman start
|
Loading…
Reference in a new issue