start implementing an api for the commands

This commit is contained in:
lyx0 2022-08-16 16:35:45 +02:00
parent 003a3737ac
commit 3843c76906
10 changed files with 267 additions and 5 deletions

4
.gitignore vendored
View file

@ -16,3 +16,7 @@
.env
Nourybot
Nourybot-Api
nourybot-api
Nourybot-Web
nourybot-web

View file

@ -9,3 +9,6 @@ xd:
jq:
cd cmd/bot && go build -o Nourybot && ./Nourybot | jq
jqapi:
cd cmd/api && go build -o Nourybot-Api && ./Nourybot-Api | jq

64
cmd/api/command.go Normal file
View file

@ -0,0 +1,64 @@
package main
import (
"encoding/json"
"errors"
"net/http"
"github.com/lyx0/nourybot/internal/data"
)
func (app *application) showCommandHandler(w http.ResponseWriter, r *http.Request) {
name, err := app.readCommandNameParam(r)
if err != nil {
// app.Logger.Errorf("showCommandHandler, Command not found", err)
return
}
// Get the data for a specific movie from our helper method,
// then check if an error was returned, and which.
command, err := app.Models.Commands.Get(name)
if err != nil {
switch {
case errors.Is(err, data.ErrRecordNotFound):
app.Logger.Errorf("showCommandHandler, Command not found", err)
return
default:
app.serverErrorResponse(w, r, err)
}
return
}
app.Logger.Info("Command Name:", command.Name)
err = app.writeJSON(w, http.StatusOK, envelope{"movie": command}, nil)
// if err != nil {
// app.serverErrorResponse(w, r, err)
// }
}
type envelope map[string]interface{}
func (app *application) writeJSON(w http.ResponseWriter, status int, data envelope, headers http.Header) error {
// Encode the data into JSON and return any errors if there were any.
// Use MarshalIndent instead of normal Marshal so it looks prettier on terminals.
js, err := json.MarshalIndent(data, "", "\t")
if err != nil {
return err
}
// Append a newline to make it prettier on terminals.
js = append(js, '\n')
// Iterate over the header map and add each header to the
// http.ResponseWriter header map.
for key, value := range headers {
w.Header()[key] = value
}
// Set `Content-Type` to `application/json` because go
// defaults to `text-plain; charset=utf8`.
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
w.Write(js)
return nil
}

29
cmd/api/errors.go Normal file
View file

@ -0,0 +1,29 @@
package main
import (
"fmt"
"net/http"
)
func (app *application) serverErrorResponse(w http.ResponseWriter, r *http.Request, err error) {
app.logError(r, err)
message := "the server encountered a problem and could not process your request"
app.errorResponse(w, r, http.StatusInternalServerError, message)
}
// The logError() method is a generic helper for logging an error message. Later in the
// book we'll upgrade this to use structured logging, and record additional information
// about the request including the HTTP method and URL.
func (app *application) logError(r *http.Request, err error) {
app.Logger.Infow("logError",
"error", err)
}
func (app *application) errorResponse(w http.ResponseWriter, r *http.Request, status int, message interface{}) {
// Write the response using the writeJSON() helper. If this happens to return an
// error then log it, and fall back to sending the client an empty response with a
// 500 Internal Server Error status code.
fmt.Fprintf(w, "Error: %s", status)
}

35
cmd/api/helpers.go Normal file
View file

@ -0,0 +1,35 @@
package main
import (
"errors"
"net/http"
"strconv"
"github.com/julienschmidt/httprouter"
)
func (app *application) readIDParam(r *http.Request) (int64, error) {
params := httprouter.ParamsFromContext(r.Context())
// Use `ByName()` function to get the value of the "id" parameter from the slice.
// The value returned by `ByName()` is always a string so we try to convert it to
// base64 with a bit size of 64.
id, err := strconv.ParseInt(params.ByName("id"), 10, 64)
if err != nil || id < 1 {
return 0, errors.New("invalid id parameter")
}
return id, nil
}
func (app *application) readCommandNameParam(r *http.Request) (string, error) {
params := httprouter.ParamsFromContext(r.Context())
app.Logger.Info(r.Context())
// Use `ByName()` function to get the value of the "id" parameter from the slice.
// The value returned by `ByName()` is always a string so we try to convert it to
// base64 with a bit size of 64.
name := params.ByName("name")
return name, nil
}

111
cmd/api/main.go Normal file
View file

@ -0,0 +1,111 @@
package main
import (
"context"
"database/sql"
"fmt"
"log"
"net/http"
"os"
"time"
"github.com/joho/godotenv"
_ "github.com/lib/pq"
"github.com/lyx0/nourybot/internal/data"
"go.uber.org/zap"
)
type config struct {
port int
db struct {
dsn string
maxOpenConns int
maxIdleConns int
maxIdleTime string
}
}
type application struct {
Logger *zap.SugaredLogger
Db *sql.DB
Models data.Models
}
func main() {
var cfg config
// Initialize a new sugared logger that we'll pass on
// down through the application.
sugar := zap.NewExample().Sugar()
defer sugar.Sync()
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
// Database
cfg.db.dsn = os.Getenv("DB_DSN")
cfg.port = 3000
cfg.db.maxOpenConns = 25
cfg.db.maxIdleConns = 25
cfg.db.maxIdleTime = "15m"
// Establish database connection
db, err := openDB(cfg)
if err != nil {
sugar.Fatal(err)
}
// Initialize Application
app := &application{
Logger: sugar,
Db: db,
Models: data.NewModels(db),
}
srv := &http.Server{
Addr: fmt.Sprintf(":%d", cfg.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
}
err = srv.ListenAndServe()
sugar.Fatal(err)
}
// openDB returns the sql.DB connection pool.
func openDB(cfg config) (*sql.DB, error) {
// sql.Open() creates an empty connection pool with the provided DSN
db, err := sql.Open("postgres", cfg.db.dsn)
if err != nil {
return nil, err
}
// Set database restraints.
db.SetMaxOpenConns(cfg.db.maxOpenConns)
db.SetMaxIdleConns(cfg.db.maxIdleConns)
// Parse the maxIdleTime string into an actual duration and set it.
duration, err := time.ParseDuration(cfg.db.maxIdleTime)
if err != nil {
return nil, err
}
db.SetConnMaxIdleTime(duration)
// Create a new context with a 5 second timeout.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// db.PingContext() is needed to actually check if the
// connection to the database was successful.
err = db.PingContext(ctx)
if err != nil {
return nil, err
}
return db, nil
}

14
cmd/api/routes.go Normal file
View file

@ -0,0 +1,14 @@
package main
import (
"net/http"
"github.com/julienschmidt/httprouter"
)
func (app *application) routes() *httprouter.Router {
router := httprouter.New()
router.HandlerFunc(http.MethodGet, "/v1/commands/:name", app.showCommandHandler)
return router
}

1
go.mod
View file

@ -7,6 +7,7 @@ require (
github.com/dustin/go-humanize v1.0.0
github.com/gempir/go-twitch-irc/v3 v3.2.0
github.com/joho/godotenv v1.4.0
github.com/julienschmidt/httprouter v1.3.0
github.com/lib/pq v1.10.6
go.uber.org/zap v1.21.0
)

2
go.sum
View file

@ -11,6 +11,8 @@ github.com/gempir/go-twitch-irc/v3 v3.2.0 h1:ENhsa7RgBE1GMmDqe0iMkvcSYfgw6ZsXilt
github.com/gempir/go-twitch-irc/v3 v3.2.0/go.mod h1:/W9KZIiyizVecp4PEb7kc4AlIyXKiCmvlXrzlpPUytU=
github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=

View file

@ -14,12 +14,11 @@ const Home: NextPage = () => {
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
Nourybot
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.tsx</code>
A lidl Twitch bot made by <a href="https://twitch.tv/nourylul">Noury</a>
</p>
<div className={styles.grid}>