Compare commits

...

47 Commits

Author SHA1 Message Date
1b36ad9040 Merge pull request 'Generate client code based on the Swagger > OpenApi document' (#13) from features/generate-client-api into main
Reviewed-on: #13
2024-07-17 22:04:34 -07:00
3cf6afb39c updated make to allow me to convert swagger into a api client with some tools 2024-07-17 22:02:38 -07:00
fa1e762f51 made a script to convert swagger to a openapi document 2024-07-17 22:01:58 -07:00
27ead1c68a cleaned up swagger comments 2024-07-17 22:01:29 -07:00
f4e4fbdee2 fixed a query pulling source 2024-07-17 22:00:57 -07:00
56199a795a Merge pull request 'moving new rss to accept a body request' (#12) from features/new-source-body into main
Reviewed-on: #12
2024-07-10 07:23:35 -07:00
5114332d5a moving new rss to accept a body request 2024-07-10 07:23:01 -07:00
b94257f5aa Merge pull request 'fixed the route to pull sourceById' (#11) from features/sourceById into main
Reviewed-on: #11
2024-06-02 19:54:21 -07:00
9ebfea122c fixed the route to pull sourceById 2024-06-02 19:54:04 -07:00
9237369e5a Merge pull request 'updated handlers to define if the response was an error' (#10) from features/baseMessage-defines-isError into main
Reviewed-on: #10
2024-06-02 17:28:09 -07:00
c05e7c3aea updated handlers to define if the response was an error 2024-06-02 17:27:36 -07:00
877a6a9619 Merge pull request 'sqlc removed and adding sessionToken to jwt' (#9) from features/sessiontokens into main
Reviewed-on: #9
2024-05-26 09:02:18 -07:00
7d4648e78b sqlc removed and adding sessionToken to jwt 2024-05-26 09:00:33 -07:00
47058dd866 sqlc removed and adding sessionToken to jwt 2024-05-26 07:52:29 -07:00
4e9a17209f Merge pull request 'features/move-domain-for-portal' (#8) from features/move-domain-for-portal into main
Reviewed-on: #8
2024-05-09 19:10:03 -07:00
a818b892f4 domain path update for source const 2024-05-09 19:09:32 -07:00
d7f2eca4c3 repo services not use entity 2024-05-09 19:09:11 -07:00
80da61db8c repositories now use the new entity package 2024-05-09 19:08:57 -07:00
e9e208371a handlers got updated for the new dtoconv package 2024-05-09 19:08:43 -07:00
6bccbce91b domain was broken up for external usage 2024-05-09 19:08:25 -07:00
94c2bdb312 entity was moved to its own page 2024-05-09 18:59:50 -07:00
5b8cf6dfa6 Merge pull request 'features/jwt' (#7) from features/jwt into main
Reviewed-on: #7
2024-05-07 22:21:57 -07:00
e38643938a dumb rename and attempting to get CI working 2024-05-07 22:20:50 -07:00
c0fb43df7d More testing done around jwt and things are looking ok. Should be able to work on the portal now some. 2024-05-07 22:10:17 -07:00
5ff6a8ddae updated error handling and refined how the jwt gets used and validated 2024-05-07 22:01:32 -07:00
471ef4fdd8 adding userId to the jwt and updated routes to use scope requirements 2024-05-07 19:14:37 -07:00
c539a20cc7 got the user routes exposed with swagger, added jwt support to swagger and also updated how the scopes are validated 2024-05-07 18:19:41 -07:00
c765227932 Getting the user and jwt stuff added to the api. Now to get swagger working 2024-05-05 10:02:17 -07:00
53b0469647 discordwebhooks now bind to a user id. More work to do though 2024-05-04 11:58:35 -07:00
f0d36eb2ab Refactored some tables to remove subscriptions and break out the naming 2024-05-04 11:58:10 -07:00
238f617452 jwt was added and started to tie it into handlers 2024-05-04 11:47:30 -07:00
db9b0bbb1d Removed the settings handler because its not needed anymore 2024-05-04 11:47:02 -07:00
e57b115117 Merge pull request 'features/rss-job' (#6) from features/rss-job into main
Reviewed-on: #6
2024-05-02 17:41:11 -07:00
fb471659d9 minor table touches like related ID's and updated seed 2024-05-02 17:39:07 -07:00
20ec963e78 minor updates 2024-05-02 17:38:30 -07:00
4eff80c1fd minor cleanup 2024-05-02 17:37:35 -07:00
f1f2142fdf correct rss to null check the author 2024-05-02 17:37:18 -07:00
852db161c9 changed how cron uses context 2024-05-02 17:36:56 -07:00
e523cc72d1 updated swagger defs and removed old dto package 2024-05-02 17:36:39 -07:00
781dced4ef removed the old complex dto package 2024-05-02 17:36:20 -07:00
9b2cd646b8 chi has been removed 2024-05-01 18:27:55 -07:00
62fa7224c8 minor changes to make and gitignore 2024-05-01 18:27:19 -07:00
c44d7f20bc server passes in the db to cron now 2024-05-01 18:27:03 -07:00
ddf50077b5 cron is now cleaned up and only running rss for now 2024-05-01 18:26:32 -07:00
77b5c2bfa4 inputs now use the domain package 2024-05-01 18:26:14 -07:00
bfa0f1023d got the rss job setup and disabled the others for now 2024-05-01 17:49:38 -07:00
43f42d9db0 Merge pull request 'features/handler-updates' (#5) from features/handler-updates into main
Reviewed-on: #5
2024-04-28 19:30:09 -07:00
84 changed files with 8507 additions and 5241 deletions

60
.drone.yaml Normal file
View File

@ -0,0 +1,60 @@
---
kind: pipeline
type: docker
name: buildLatestImage
steps:
- name: buildLatestImage
image: plugins/docker
settings:
repo: jtom38/newsbot-collector
username: jtom38
password:
from_secret: DockerPushPat
trigger:
branch:
include:
- main
event:
exclude:
- pull_request
---
kind: pipeline
type: docker
name: buildReleaseImage
steps:
- name: buildReleaseImage
image: plugins/docker
settings:
repo: jtom38/newsbot-collector
username: jtom38
password:
from_secret: DockerPushPat
trigger:
branch:
include:
- releases/*
ref:
include:
- refs/tags/**
event:
exclude:
- pull_request
---
kind: pipeline
type: docker
name: PullRequestCompileTest
steps:
- name: Compile project
image: golang:1.22
commands:
- go test ./internal/repository
- go build ./cmd/server.go
-
trigger:
event:
- pull_request

3
.gitignore vendored
View File

@ -3,6 +3,7 @@ dev.session.sql
__debug_bin __debug_bin
server server
.vscode .vscode
openapi.json
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe
@ -11,6 +12,8 @@ server
*.so *.so
*.dylib *.dylib
collector collector
newsbot.db
tmp/
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test

15
.vscode/launch.json vendored
View File

@ -1,15 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Launch Package",
"type": "go",
"request": "launch",
"mode": "auto",
"program": "."
}
]
}

View File

@ -5,14 +5,10 @@ WORKDIR /app
# Always make sure that swagger docs are updated # Always make sure that swagger docs are updated
RUN go install github.com/swaggo/swag/cmd/swag@latest RUN go install github.com/swaggo/swag/cmd/swag@latest
RUN /go/bin/swag i RUN /go/bin/swag init -g cmd/server.go
# Always build the latest sql queries #RUN go build .
RUN go install github.com/kyleconroy/sqlc/cmd/sqlc@latest #RUN go install github.com/pressly/goose/v3/cmd/goose@latest
RUN /go/bin/sqlc generate
RUN go build .
RUN go install github.com/pressly/goose/v3/cmd/goose@latest
FROM alpine:latest as app FROM alpine:latest as app
@ -21,8 +17,7 @@ RUN apk --no-cache add libc6-compat
RUN apk --no-cache add chromium RUN apk --no-cache add chromium
RUN mkdir /app && mkdir /app/migrations RUN mkdir /app && mkdir /app/migrations
COPY --from=build /app/collector /app COPY --from=build /app/server /app
COPY --from=build /go/bin/goose /app COPY ./internal/database/migrations/ /app/migrations
COPY ./database/migrations/ /app/migrations
CMD [ "/app/collector" ] CMD [ "/app/collector" ]

6
api.http Normal file
View File

@ -0,0 +1,6 @@
### Select Sources fro mthe top
GET http://localhost:8081/api/v1/sources/
### Select Sources by type
GET http://localhost:8081/api/v1/sources/by/source?source=rss

3756
api/api.gen.go Normal file

File diff suppressed because it is too large Load Diff

6
api/client.yaml Normal file
View File

@ -0,0 +1,6 @@
# yaml-language-server: $schema=https://raw.githubusercontent.com/deepmap/oapi-codegen/HEAD/configuration-schema.json
package: api
output: api.gen.go
generate:
models: true
client: true

View File

@ -3,15 +3,15 @@ package main
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"fmt" "fmt"
"net/http" "os"
_ "github.com/glebarez/go-sqlite" _ "github.com/glebarez/go-sqlite"
"github.com/pressly/goose/v3" "github.com/pressly/goose/v3"
"git.jamestombleson.com/jtom38/newsbot-api/docs" "git.jamestombleson.com/jtom38/newsbot-api/docs"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" v1 "git.jamestombleson.com/jtom38/newsbot-api/internal/handler/v1"
"git.jamestombleson.com/jtom38/newsbot-api/internal/handler/v1"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/cron" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/cron"
) )
@ -19,6 +19,10 @@ import (
// @title NewsBot collector // @title NewsBot collector
// @version 0.1 // @version 0.1
// @BasePath /api // @BasePath /api
// @securityDefinitions.apikey Bearer
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() { func main() {
ctx := context.Background() ctx := context.Background()
@ -32,7 +36,25 @@ func main() {
panic(err) panic(err)
} }
err = goose.SetDialect("sqlite3") err = migrateDatabase(db)
if err != nil {
fmt.Print(err)
}
c := cron.NewScheduler(ctx, db)
c.Start()
server := v1.NewServer(ctx, configs, db)
fmt.Println("API is online and waiting for requests.")
fmt.Printf("API: http://%v:8081/api\r\n", configs.ServerAddress)
fmt.Printf("Swagger: http://%v:8081/swagger/index.html\r\n", configs.ServerAddress)
server.Router.Start(":8081")
}
func migrateDatabase(db *sql.DB) error {
err := goose.SetDialect("sqlite3")
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -42,19 +64,27 @@ func main() {
panic(err) panic(err)
} }
queries := database.New(db) _, err = os.Stat("./migrations")
if err == nil {
c := cron.NewScheduler(ctx) err = goose.Up(db, "../internal/database/migrations")
c.Start()
server := v1.NewServer(ctx, queries, configs, db)
fmt.Println("API is online and waiting for requests.")
fmt.Printf("API: http://%v:8081/api\r\n", configs.ServerAddress)
fmt.Printf("Swagger: http://%v:8081/swagger/index.html\r\n", configs.ServerAddress)
err = http.ListenAndServe(":8081", server.Router)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return nil
}
_, err = os.Stat("../internal/database/migrations")
if err == nil {
err = goose.Up(db, "../internal/database/migrations")
if err != nil {
panic(err)
}
return nil
}
return errors.New("failed to find the migration files")
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

27
domain/requests.go Normal file
View File

@ -0,0 +1,27 @@
package domain
type LoginFormRequest struct {
Username string `form:"username"`
Password string `form:"password"`
}
type GetSourceBySourceAndNameParamRequest struct {
Name string `query:"name"`
Source string `query:"source"`
}
type NewSourceParamRequest struct {
Name string `json:"name"`
Url string `json:"url"`
Tags string `json:"tags"`
}
type RefreshTokenRequest struct {
Username string `json:"username"`
RefreshToken string `json:"refreshToken"`
}
type UpdateScopesRequest struct {
Username string `json:"username"`
Scopes []string `json:"scopes" validate:"required"`
}

View File

@ -1,8 +1,15 @@
package domain package domain
type BaseResponse struct { type BaseResponse struct {
Message string `json:"message"` Message string `json:"message"`
IsError bool `json:"isError"`
}
type LoginResponse struct {
BaseResponse
Token string `json:"token"`
Type string `json:"type"`
RefreshToken string `json:"refreshToken"`
} }
type ArticleResponse struct { type ArticleResponse struct {

14
domain/scopes.go Normal file
View File

@ -0,0 +1,14 @@
package domain
const (
ScopeAll = "newsbot:all"
ScopeArticleRead = "newsbot:article:read"
ScopeArticleDisable = "newsbot:article:disable"
ScopeSourceRead = "newsbot:source:read"
ScopeSourceCreate = "newsbot:source:create"
ScopeDiscordWebHookCreate = "newsbot:discordwebhook:create"
ScopeDiscordWebhookRead = "newsbot:discordwebhook:read"
)

5
go.mod
View File

@ -5,11 +5,12 @@ go 1.22
require ( require (
github.com/PuerkitoBio/goquery v1.8.0 github.com/PuerkitoBio/goquery v1.8.0
github.com/glebarez/go-sqlite v1.22.0 github.com/glebarez/go-sqlite v1.22.0
github.com/go-chi/chi/v5 v5.0.7
github.com/go-rod/rod v0.107.1 github.com/go-rod/rod v0.107.1
github.com/golang-jwt/jwt/v5 v5.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/huandu/go-sqlbuilder v1.27.1 github.com/huandu/go-sqlbuilder v1.27.1
github.com/joho/godotenv v1.4.0 github.com/joho/godotenv v1.4.0
github.com/labstack/echo-jwt/v4 v4.2.0
github.com/labstack/echo/v4 v4.12.0 github.com/labstack/echo/v4 v4.12.0
github.com/mmcdole/gofeed v1.1.3 github.com/mmcdole/gofeed v1.1.3
github.com/nicklaw5/helix/v2 v2.4.0 github.com/nicklaw5/helix/v2 v2.4.0
@ -23,6 +24,7 @@ require (
require ( require (
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ghodss/yaml v1.0.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/labstack/gommon v0.4.2 // indirect github.com/labstack/gommon v0.4.2 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-colorable v0.1.13 // indirect
@ -36,6 +38,7 @@ require (
github.com/valyala/fasttemplate v1.2.2 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
golang.org/x/sync v0.7.0 // indirect golang.org/x/sync v0.7.0 // indirect
golang.org/x/time v0.5.0 // indirect
modernc.org/libc v1.41.0 // indirect modernc.org/libc v1.41.0 // indirect
modernc.org/mathutil v1.6.0 // indirect modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.7.2 // indirect modernc.org/memory v1.7.2 // indirect

10
go.sum
View File

@ -18,8 +18,6 @@ github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ= github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc= github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8=
github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -33,8 +31,12 @@ github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrK
github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-rod/rod v0.107.1 h1:wRxTTAXJ0JUnoSGcyGAOubpdrToWIKPCnLu3av8EDFY= github.com/go-rod/rod v0.107.1 h1:wRxTTAXJ0JUnoSGcyGAOubpdrToWIKPCnLu3av8EDFY=
github.com/go-rod/rod v0.107.1/go.mod h1:Au6ufsz7KyXUJVnw6Ljs1nFpsopy+9AJ/lBwGauYBVg= github.com/go-rod/rod v0.107.1/go.mod h1:Au6ufsz7KyXUJVnw6Ljs1nFpsopy+9AJ/lBwGauYBVg=
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk=
github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
@ -60,6 +62,8 @@ 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= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM= github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
@ -163,6 +167,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps=

View File

@ -1,31 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
package database
import (
"context"
"database/sql"
)
type DBTX interface {
ExecContext(context.Context, string, ...interface{}) (sql.Result, error)
PrepareContext(context.Context, string) (*sql.Stmt, error)
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error)
QueryRowContext(context.Context, string, ...interface{}) *sql.Row
}
func New(db DBTX) *Queries {
return &Queries{db: db}
}
type Queries struct {
db DBTX
}
func (q *Queries) WithTx(tx *sql.Tx) *Queries {
return &Queries{
db: tx,
}
}

View File

@ -1,45 +0,0 @@
package database
import (
"strings"
"github.com/google/uuid"
)
type SourceDto struct {
ID uuid.UUID `json:"id"`
Site string `json:"site"`
Name string `json:"name"`
Source string `json:"source"`
Type string `json:"type"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
Url string `json:"url"`
Tags []string `json:"tags"`
Deleted bool `json:"deleted"`
}
func ConvertToSourceDto(i Source) SourceDto {
var deleted bool
if !i.Deleted.Valid {
deleted = true
}
return SourceDto{
ID: i.ID,
Site: i.Site,
Name: i.Name,
Source: i.Source,
Type: i.Type,
Value: i.Value.String,
Enabled: i.Enabled,
Url: i.Url,
Tags: splitTags(i.Tags),
Deleted: deleted,
}
}
func splitTags(t string) []string {
items := strings.Split(t, ", ")
return items
}

View File

@ -12,30 +12,18 @@ CREATE TABLE Articles (
Url TEXT NOT NULL, Url TEXT NOT NULL,
PubDate DATETIME NOT NULL, PubDate DATETIME NOT NULL,
IsVideo TEXT NOT NULL, IsVideo TEXT NOT NULL,
--VideoHeight int NOT NULL,
--VideoWidth int NOT NULL,
ThumbnailUrl TEXT NOT NULL, ThumbnailUrl TEXT NOT NULL,
Description TEXT NOT NULL, Description TEXT NOT NULL,
AuthorName TEXT NOT NULL, AuthorName TEXT NOT NULL,
AuthorImageUrl TEXT NOT NULL AuthorImageUrl TEXT NOT NULL
); );
CREATE Table DiscordQueue (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME,
ArticleId NUMBER NOT NULL,
SourceId NUMBER NOT NULL
);
CREATE Table DiscordWebHooks ( CREATE Table DiscordWebHooks (
ID INTEGER PRIMARY KEY AUTOINCREMENT, ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL, CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME NOT NULL, DeletedAt DATETIME NOT NULL,
--Name TEXT NOT NULL, -- Defines webhook purpose UserID INTEGER NOT NULL,
--Key TEXT,
Url TEXT NOT NULL, -- Webhook Url Url TEXT NOT NULL, -- Webhook Url
Server TEXT NOT NULL, -- Defines the server its bound it. Used for reference Server TEXT NOT NULL, -- Defines the server its bound it. Used for reference
Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for reference Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for reference
@ -73,20 +61,30 @@ CREATE Table Sources (
Tags TEXT NOT NULL Tags TEXT NOT NULL
); );
CREATE TABLE Subscriptions ( CREATE TABLE UserSourceSubscriptions (
ID INTEGER PRIMARY KEY AUTOINCREMENT, ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL, CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME, DeletedAt DATETIME NOT NULL,
DiscordWebHookId NUMBER NOT NULL, UserID NUMBER NOT NULL,
SourceId NUMBER NOT NULL SourceID NUMBER NOT NULL
);
CREATE TABLE AlertDiscord (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME NOT NULL,
UserID NUMBER NOT NULL,
SourceID NUMBER NOT NULL,
DiscordWebHookID NUMBER NOT NULL
); );
CREATE TABLE Users ( CREATE TABLE Users (
ID INTEGER PRIMARY KEY AUTOINCREMENT, ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL, CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME, DeletedAt DATETIME NOT NULL,
Name TEXT NOT NULL, Name TEXT NOT NULL,
Hash TEXT NOT NULL, Hash TEXT NOT NULL,
Scopes TEXT NOT NULL Scopes TEXT NOT NULL
@ -96,7 +94,7 @@ CREATE TABLE RefreshTokens (
ID INTEGER PRIMARY KEY AUTOINCREMENT, ID INTEGER PRIMARY KEY AUTOINCREMENT,
CreatedAt DATETIME NOT NULL, CreatedAt DATETIME NOT NULL,
UpdatedAt DATETIME NOT NULL, UpdatedAt DATETIME NOT NULL,
DeletedAt DATETIME, DeletedAt DATETIME NOT NULL,
Username TEXT NOT NULL, Username TEXT NOT NULL,
Token TEXT NOT NULL Token TEXT NOT NULL
); );
@ -106,13 +104,12 @@ CREATE TABLE RefreshTokens (
-- +goose Down -- +goose Down
-- +goose StatementBegin -- +goose StatementBegin
DROP TABLE AlertDiscord;
Drop Table Articles; Drop Table Articles;
Drop Table DiscordQueue;
Drop Table DiscordWebHooks; Drop Table DiscordWebHooks;
Drop Table Icons; Drop Table Icons;
Drop Table Settings;
Drop Table Sources;
DROP TABLE Subscriptions;
DROP TABLE Users;
DROP TABLE RefreshTokens; DROP TABLE RefreshTokens;
Drop Table Sources;
DROP TABLE Users;
DROP TABLE UserSourceSubscriptions;
-- +goose StatementEnd -- +goose StatementEnd

View File

@ -7,33 +7,33 @@ SELECT 'up SQL query';
-- Final Fantasy XIV Entries -- Final Fantasy XIV Entries
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - NA', TRUE, 'https://na.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, na, lodestone'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Final Fantasy XIV - NA', 'ffxiv', TRUE, 'https://na.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, na, lodestone');
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - JP', FALSE, 'https://jp.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, jp, lodestone'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Final Fantasy XIV - JP', 'ffxiv', FALSE, 'https://jp.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, jp, lodestone');
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - EU', FALSE, 'https://eu.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, eu, lodestone'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Final Fantasy XIV - EU', 'ffxiv', FALSE, 'https://eu.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, eu, lodestone');
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - FR', FALSE, 'https://fr.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, fr, lodestone'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Final Fantasy XIV - FR', 'ffxiv', FALSE, 'https://fr.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, fr, lodestone');
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'ffxiv', 'Final Fantasy XIV - DE', FALSE, 'https://de.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, de, lodestone'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Final Fantasy XIV - DE', 'ffxiv', FALSE, 'https://de.finalfantasyxiv.com/lodestone/', 'ffxiv, final, fantasy, xiv, de, lodestone');
-- Reddit Entries -- Reddit Entries
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'reddit', 'dadjokes', TRUE, 'https://reddit.com/r/dadjokes', 'reddit, dadjokes'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'dadjokes', 'reddit', TRUE, 'https://reddit.com/r/dadjokes', 'reddit, dadjokes');
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'reddit', 'steamdeck', TRUE, 'https://reddit.com/r/steamdeck', 'reddit, steam deck, steam, deck'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'steamdeck', 'reddit', TRUE, 'https://reddit.com/r/steamdeck', 'reddit, steam deck, steam, deck');
-- Youtube Entries -- Youtube Entries
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'youtube', 'Game Grumps', TRUE, 'https://www.youtube.com/user/GameGrumps', 'youtube, game grumps, game, grumps'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Game Grumps', 'youtube', TRUE, 'https://www.youtube.com/user/GameGrumps', 'youtube, game grumps, game, grumps');
-- RSS Entries -- RSS Entries
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'steampowered', 'steam deck', TRUE, 'https://store.steampowered.com/feeds/news/app/1675200/?cc=US&l=english&snr=1_2108_9__2107', 'rss, steampowered, steam, deck, steam deck'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'steampowered - steam deck', 'rss', TRUE, 'https://store.steampowered.com/feeds/news/app/1675200/?cc=US&l=english&snr=1_2108_9__2107', 'rss, steampowered, steam, deck, steam deck');
-- Twitch Entries -- Twitch Entries
INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabled, Url, Tags) VALUES
("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'twitch', 'Nintendo', TRUE, 'https://twitch.tv/nintendo', 'twitch, nintendo'); ("2024-04-25 18:37:43.852367", "2024-04-25 18:37:43.852367", "0001-01-01 00:00:00", 'Nintendo', 'twitch', TRUE, 'https://twitch.tv/nintendo', 'twitch, nintendo');
-- +goose StatementEnd -- +goose StatementEnd
@ -41,10 +41,10 @@ INSERT INTO sources (CreatedAt, UpdatedAt, DeletedAt, DisplayName, Source, Enabl
-- +goose StatementBegin -- +goose StatementBegin
--SELECT 'down SQL query'; --SELECT 'down SQL query';
DELETE FROM sources where source = 'reddit' and name = 'dadjokes'; DELETE FROM sources where Source = 'reddit' and DisplayName = 'dadjokes';
DELETE FROM sources where source = 'reddit' and name = 'steamdeck'; DELETE FROM sources where Source = 'reddit' and DisplayName = 'steamdeck';
DELETE FROM sources where source = 'ffxiv'; DELETE FROM sources where Source = 'ffxiv';
DELETE FROM sources WHERE source = 'twitch' and name = 'Nintendo'; DELETE FROM sources WHERE Source = 'twitch' and DisplayName = 'Nintendo';
DELETE FROM sources WHERE source = 'youtube' and name = 'Game Grumps'; DELETE FROM sources WHERE Source = 'youtube' and DisplayName = 'Game Grumps';
DELETE FROM SOURCES WHERE source = 'rss' and name = 'steam deck'; DELETE FROM SOURCES WHERE Source = 'rss' and DisplayName = 'steampowered - steam deck';
-- +goose StatementEnd -- +goose StatementEnd

View File

@ -0,0 +1,9 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE Users ADD SessionToken TEXT;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE Users DROP SessionToken;
-- +goose StatementEnd

View File

@ -1,73 +0,0 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.16.0
package database
import (
"database/sql"
"time"
"github.com/google/uuid"
)
type Article struct {
ID uuid.UUID
Sourceid uuid.UUID
Tags string
Title string
Url string
Pubdate time.Time
Video sql.NullString
Videoheight int32
Videowidth int32
Thumbnail string
Description string
Authorname sql.NullString
Authorimage sql.NullString
}
type Discordqueue struct {
ID uuid.UUID
Articleid uuid.UUID
}
type Discordwebhook struct {
ID uuid.UUID
Url string
Server string
Channel string
Enabled bool
}
type Icon struct {
ID uuid.UUID
Filename string
Site string
}
type Setting struct {
ID uuid.UUID
Key string
Value string
Options sql.NullString
}
type Source struct {
ID uuid.UUID
Site string
Name string
Source string
Type string
Value sql.NullString
Enabled bool
Url string
Tags string
Deleted sql.NullBool
}
type Subscription struct {
ID uuid.UUID
Discordwebhookid uuid.UUID
Sourceid uuid.UUID
}

File diff suppressed because it is too large Load Diff

View File

@ -1,215 +0,0 @@
/* Articles */
-- name: GetArticleByID :one
Select * from Articles
WHERE ID = $1 LIMIT 1;
-- name: GetArticleByUrl :one
Select * from Articles
Where Url = $1 LIMIT 1;
-- name: ListArticles :many
Select * From articles
Order By PubDate DESC
offset $2
fetch next $1 rows only;
-- name: ListArticlesByDate :many
Select * From articles
ORDER BY pubdate desc
Limit $1;
-- name: GetArticlesBySource :many
select * from articles
INNER join sources on articles.sourceid=Sources.ID
where site = $1;
-- name: ListNewArticlesBySourceId :many
SELECT * FROM articles
Where sourceid = $1
ORDER BY pubdate desc
offset $3
fetch next $2 rows only;
-- name: ListOldestArticlesBySourceId :many
SELECT * FROM articles
Where sourceid = $1
ORDER BY pubdate asc
offset $3
fetch next $2 rows only;
-- name: ListArticlesBySourceId :many
Select * From articles
Where sourceid = $1
Limit 50;
-- name: GetArticlesBySourceName :many
select
articles.ID, articles.SourceId, articles.Tags, articles.Title, articles.Url, articles.PubDate, articles.Video, articles.VideoHeight, articles.VideoWidth, articles.Thumbnail, articles.Description, articles.AuthorName, articles.AuthorImage, sources.source, sources.name
From articles
Left Join sources
On articles.sourceid = sources.id
Where name = $1;
-- name: ListArticlesByPage :many
select * from articles
order by pubdate desc
offset $2
fetch next $1 rows only;
-- name: CreateArticle :exec
INSERT INTO Articles
(ID, SourceId, Tags, Title, Url, PubDate, Video, VideoHeight, VideoWidth, Thumbnail, Description, AuthorName, AuthorImage)
Values
($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13);
/* DiscordQueue */
-- name: CreateDiscordQueue :exec
Insert into DiscordQueue
(ID, ArticleId)
Values
($1, $2);
-- name: GetDiscordQueueByID :one
Select * from DiscordQueue
Where ID = $1 LIMIT 1;
-- name: DeleteDiscordQueueItem :exec
Delete From DiscordQueue Where ID = $1;
-- name: ListDiscordQueueItems :many
Select * from DiscordQueue LIMIT $1;
/* DiscordWebHooks */
-- name: CreateDiscordWebHook :exec
Insert Into DiscordWebHooks
(ID, Url, Server, Channel, Enabled)
Values
($1, $2, $3, $4, $5);
-- name: GetDiscordWebHooksByID :one
Select * from DiscordWebHooks
Where ID = $1 LIMIT 1;
-- name: ListDiscordWebHooksByServer :many
Select * From DiscordWebHooks
Where Server = $1;
-- name: GetDiscordWebHooksByServerAndChannel :many
SELECT * FROM DiscordWebHooks
WHERE Server = $1 and Channel = $2;
-- name: GetDiscordWebHookByUrl :one
Select * From DiscordWebHooks Where url = $1;
-- name: ListDiscordWebhooks :many
Select * From discordwebhooks LIMIT $1;
-- name: DeleteDiscordWebHooks :exec
Delete From discordwebhooks Where ID = $1;
-- name: DisableDiscordWebHook :exec
Update discordwebhooks Set Enabled = FALSE where ID = $1;
-- name: EnableDiscordWebHook :exec
Update discordwebhooks Set Enabled = TRUE where ID = $1;
/* Icons */
-- name: CreateIcon :exec
INSERT INTO Icons
(ID, FileName, Site)
VALUES
($1,$2,$3);
-- name: GetIconByID :one
Select * FROM Icons
Where ID = $1 Limit 1;
-- name: GetIconBySite :one
Select * FROM Icons
Where Site = $1 Limit 1;
-- name: DeleteIcon :exec
Delete From Icons where ID = $1;
/* Settings */
-- name: CreateSettings :one
Insert Into settings
(ID, Key, Value, OPTIONS)
Values
($1,$2,$3,$4)
RETURNING *;
-- name: GetSettingByID :one
Select * From settings
Where ID = $1 Limit 1;
-- name: GetSettingByKey :one
Select * From settings Where
Key = $1 Limit 1;
-- name: GetSettingByValue :one
Select * From settings Where
Value = $1 Limit 1;
-- name: DeleteSetting :exec
Delete From settings Where ID = $1;
/* Sources */
-- name: CreateSource :exec
Insert Into Sources
(ID, Site, Name, Source, Type, Value, Enabled, Url, Tags)
Values
($1,$2,$3,$4,$5,$6,$7,$8,$9);
-- name: GetSourceByID :one
Select * From Sources where ID = $1 Limit 1;
-- name: GetSourceByName :one
Select * from Sources where name = $1 Limit 1;
-- name: GetSourceByNameAndSource :one
Select * from Sources WHERE name = $1 and source = $2;
-- name: ListSources :many
Select * From Sources Limit $1;
-- name: ListSourcesBySource :many
Select * From Sources where Source = $1;
-- name: DeleteSource :exec
UPDATE Sources Set Disabled = TRUE where id = $1;
-- name: DisableSource :exec
Update Sources Set Enabled = FALSE where ID = $1;
-- name: EnableSource :exec
Update Sources Set Enabled = TRUE where ID = $1;
/* Subscriptions */
-- name: CreateSubscription :exec
Insert Into subscriptions (ID, DiscordWebHookId, SourceId) Values ($1, $2, $3);
-- name: ListSubscriptions :many
Select * From subscriptions Limit $1;
-- name: ListSubscriptionsBySourceId :many
Select * From subscriptions where sourceid = $1;
-- name: QuerySubscriptions :one
Select * From subscriptions Where discordwebhookid = $1 and sourceid = $2 Limit 1;
-- name: GetSubscriptionsBySourceID :many
Select * From subscriptions Where sourceid = $1;
-- name: GetSubscriptionsByDiscordWebHookId :many
Select * from subscriptions Where discordwebhookid = $1;
-- name: DeleteSubscription :exec
Delete From subscriptions Where id = $1;

View File

@ -1,61 +0,0 @@
CREATE TABLE Articles (
ID uuid PRIMARY KEY,
SourceId uuid NOT null,
Tags TEXT NOT NULL,
Title TEXT NOT NULL,
Url TEXT NOT NULL,
PubDate timestamp NOT NULL,
Video TEXT,
VideoHeight int NOT NULL,
VideoWidth int NOT NULL,
Thumbnail TEXT NOT NULL,
Description TEXT NOT NULL,
AuthorName TEXT,
AuthorImage TEXT
);
CREATE Table DiscordQueue (
ID uuid PRIMARY KEY,
ArticleId uuid NOT NULL
);
CREATE Table DiscordWebHooks (
ID uuid PRIMARY KEY,
Url TEXT NOT NULL, -- Webhook Url
Server TEXT NOT NULL, -- Defines the server its bound it. Used for refrence
Channel TEXT NOT NULL, -- Defines the channel its bound to. Used for refrence
Enabled BOOLEAN NOT NULL
);
CREATE Table Icons (
ID uuid PRIMARY Key,
FileName TEXT NOT NULL,
Site TEXT NOT NULL
);
Create Table Settings (
ID uuid PRIMARY Key,
Key TEXT NOT NULL,
Value TEXT NOT NULL,
Options TEXT
);
Create Table Sources (
ID uuid PRIMARY Key,
Site TEXT NOT NULL, -- Vanity name
Name TEXT NOT NULL, -- Defines the name of the source. IE: dadjokes
Source TEXT NOT NULL, -- Defines the service that will use this reocrd. IE reddit or youtube
Type TEXT NOT NULL, -- Defines what kind of feed this is. feed, user, tag
Value TEXT,
Enabled BOOLEAN NOT NULL,
Url TEXT NOT NULL,
Tags TEXT NOT NULL,
Deleted BOOLEAN
);
/* This table is used to track what the Web Hook wants to have sent by Source */;
Create TABLE Subscriptions (
ID uuid Primary Key,
DiscordWebHookID uuid Not Null,
SourceID uuid Not Null
);

View File

@ -1,22 +0,0 @@
package interfaces
import (
"github.com/go-rod/rod"
"github.com/mmcdole/gofeed"
)
type Sources interface {
CheckSource() error
PullFeed() (*gofeed.Feed, error)
GetBrowser() *rod.Browser
GetPage(parser *rod.Browser, url string) *rod.Page
ExtractThumbnail(page *rod.Page) (string, error)
ExtractPubDate(page *rod.Page) (string, error)
ExtractDescription(page *rod.Page) (string, error)
ExtractAuthor(page *rod.Page) (string, error)
ExtractAuthorImage(page *rod.Page) (string, error)
ExtractTags(page *rod.Page) (string, error)
ExtractTitle(page *rod.Page) (string, error)
}

View File

@ -1,129 +0,0 @@
package models
import (
"strings"
"time"
"github.com/google/uuid"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
)
type ArticleDto struct {
ID uuid.UUID `json:"id"`
Source uuid.UUID `json:"sourceid"`
Tags []string `json:"tags"`
Title string `json:"title"`
Url string `json:"url"`
Pubdate time.Time `json:"pubdate"`
Video string `json:"video"`
Videoheight int32 `json:"videoHeight"`
Videowidth int32 `json:"videoWidth"`
Thumbnail string `json:"thumbnail"`
Description string `json:"description"`
Authorname string `json:"authorName"`
Authorimage string `json:"authorImage"`
}
type ArticleDetailsDto struct {
ID uuid.UUID `json:"id"`
Source SourceDto `json:"source"`
Tags []string `json:"tags"`
Title string `json:"title"`
Url string `json:"url"`
Pubdate time.Time `json:"pubdate"`
Video string `json:"video"`
Videoheight int32 `json:"videoHeight"`
Videowidth int32 `json:"videoWidth"`
Thumbnail string `json:"thumbnail"`
Description string `json:"description"`
Authorname string `json:"authorName"`
Authorimage string `json:"authorImage"`
}
type DiscordWebHooksDto struct {
ID uuid.UUID `json:"ID"`
Url string `json:"url"`
Server string `json:"server"`
Channel string `json:"channel"`
Enabled bool `json:"enabled"`
}
func ConvertToDiscordWebhookDto(i database.Discordwebhook) DiscordWebHooksDto {
return DiscordWebHooksDto{
ID: i.ID,
Url: i.Url,
Server: i.Server,
Channel: i.Channel,
Enabled: i.Enabled,
}
}
type SourceDto struct {
ID uuid.UUID `json:"id"`
Site string `json:"site"`
Name string `json:"name"`
Source string `json:"source"`
Type string `json:"type"`
Value string `json:"value"`
Enabled bool `json:"enabled"`
Url string `json:"url"`
Tags []string `json:"tags"`
Deleted bool `json:"deleted"`
}
func ConvertToSourceDto(i database.Source) SourceDto {
var deleted bool
if !i.Deleted.Valid {
deleted = true
}
return SourceDto{
ID: i.ID,
Site: i.Site,
Name: i.Name,
Source: i.Source,
Type: i.Type,
Value: i.Value.String,
Enabled: i.Enabled,
Url: i.Url,
Tags: splitTags(i.Tags),
Deleted: deleted,
}
}
type DiscordQueueDto struct {
ID uuid.UUID `json:"id"`
Articleid uuid.UUID `json:"articleId"`
}
type DiscordQueueDetailsDto struct {
ID uuid.UUID `json:"id"`
Article ArticleDetailsDto `json:"article"`
}
type SubscriptionDto struct {
ID uuid.UUID `json:"id"`
DiscordWebhookId uuid.UUID `json:"discordwebhookid"`
SourceId uuid.UUID `json:"sourceid"`
}
func ConvertToSubscriptionDto(i database.Subscription) SubscriptionDto {
c := SubscriptionDto{
ID: i.ID,
DiscordWebhookId: i.Discordwebhookid,
SourceId: i.Sourceid,
}
return c
}
type SubscriptionDetailsDto struct {
ID uuid.UUID `json:"id"`
Source SourceDto `json:"source"`
DiscordWebHook DiscordWebHooksDto `json:"discordwebhook"`
}
func splitTags(t string) []string {
items := strings.Split(t, ", ")
return items
}

View File

@ -1,13 +0,0 @@
package domain
type GetSourceBySourceAndNameParamRequest struct {
Name string `query:"name"`
Source string `query:"source"`
}
type NewSourceParamRequest struct {
Name string `query:"name"`
Url string `query:"url"`
Tags string `query:"tags"`
}

View File

@ -1,8 +1,11 @@
package services package dtoconv
import "git.jamestombleson.com/jtom38/newsbot-api/internal/domain" import (
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
)
func ArticlesToDto(items []domain.ArticleEntity) []domain.ArticleDto { func ArticlesToDto(items []entity.ArticleEntity) []domain.ArticleDto {
var dtos []domain.ArticleDto var dtos []domain.ArticleDto
for _, item := range items { for _, item := range items {
dtos = append(dtos, ArticleToDto(item)) dtos = append(dtos, ArticleToDto(item))
@ -10,7 +13,7 @@ func ArticlesToDto(items []domain.ArticleEntity) []domain.ArticleDto {
return dtos return dtos
} }
func ArticleToDto(item domain.ArticleEntity) domain.ArticleDto { func ArticleToDto(item entity.ArticleEntity) domain.ArticleDto {
return domain.ArticleDto{ return domain.ArticleDto{
ID: item.ID, ID: item.ID,
SourceID: item.SourceID, SourceID: item.SourceID,
@ -26,7 +29,7 @@ func ArticleToDto(item domain.ArticleEntity) domain.ArticleDto {
} }
} }
func DiscordWebhooksToDto(items []domain.DiscordWebHookEntity) []domain.DiscordWebHookDto{ func DiscordWebhooksToDto(items []entity.DiscordWebHookEntity) []domain.DiscordWebHookDto{
var dtos []domain.DiscordWebHookDto var dtos []domain.DiscordWebHookDto
for _, item := range items { for _, item := range items {
dtos = append(dtos, DiscordWebhookToDto(item)) dtos = append(dtos, DiscordWebhookToDto(item))
@ -34,7 +37,7 @@ func DiscordWebhooksToDto(items []domain.DiscordWebHookEntity) []domain.DiscordW
return dtos return dtos
} }
func DiscordWebhookToDto(item domain.DiscordWebHookEntity) domain.DiscordWebHookDto { func DiscordWebhookToDto(item entity.DiscordWebHookEntity) domain.DiscordWebHookDto {
return domain.DiscordWebHookDto{ return domain.DiscordWebHookDto{
ID: item.ID, ID: item.ID,
Server: item.Server, Server: item.Server,
@ -44,7 +47,7 @@ func DiscordWebhookToDto(item domain.DiscordWebHookEntity) domain.DiscordWebHook
} }
} }
func SourcesToDto(items []domain.SourceEntity) []domain.SourceDto { func SourcesToDto(items []entity.SourceEntity) []domain.SourceDto {
var dtos []domain.SourceDto var dtos []domain.SourceDto
for _, item := range items { for _, item := range items {
dtos = append(dtos, SourceToDto(item)) dtos = append(dtos, SourceToDto(item))
@ -52,7 +55,7 @@ func SourcesToDto(items []domain.SourceEntity) []domain.SourceDto {
return dtos return dtos
} }
func SourceToDto(item domain.SourceEntity) domain.SourceDto { func SourceToDto(item entity.SourceEntity) domain.SourceDto {
return domain.SourceDto{ return domain.SourceDto{
ID: item.ID, ID: item.ID,
Source: item.Source, Source: item.Source,

View File

@ -1,9 +1,21 @@
package domain package entity
import ( import (
"time" "time"
) )
// This links a source to a discord webhook.
// It is owned by a user so they can remove the link
type AlertDiscordEntity struct {
ID int64
CreatedAt time.Time
UpdatedAt time.Time
DeletedAt time.Time
UserID int64
SourceID int64
DiscordWebHookId int64
}
type ArticleEntity struct { type ArticleEntity struct {
ID int64 ID int64
CreatedAt time.Time CreatedAt time.Time
@ -35,6 +47,7 @@ type DiscordWebHookEntity struct {
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt time.Time DeletedAt time.Time
UserID int64
Url string Url string
Server string Server string
Channel string Channel string
@ -83,16 +96,28 @@ type SourceEntity struct {
Enabled bool Enabled bool
} }
type SubscriptionEntity struct { //type SubscriptionEntity struct {
// ID int64
// CreatedAt time.Time
// UpdatedAt time.Time
// DeletedAt time.Time
// UserID int64
// SourceID int64
// //SourceType string
// //SourceName string
// DiscordID int64
// //DiscordName string
//}
// This defines what sources a user wants to follow.
// These will show up for the user as a front page
type UserSourceSubscriptionEntity struct {
ID int64 ID int64
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
DeletedAt time.Time DeletedAt time.Time
UserID int64
SourceID int64 SourceID int64
SourceType string
SourceName string
DiscordID int64
DiscordName string
} }
type UserEntity struct { type UserEntity struct {
@ -103,6 +128,7 @@ type UserEntity struct {
Username string Username string
Hash string Hash string
Scopes string Scopes string
SessionToken string
} }
type RefreshTokenEntity struct { type RefreshTokenEntity struct {

View File

@ -4,27 +4,34 @@ import (
"net/http" "net/http"
"strconv" "strconv"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/dtoconv"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// ListArticles // ListArticles
// @Summary Lists the top 25 records ordering from newest to oldest. // @Summary Lists the top 25 records ordering from newest to oldest.
// @Produce application/json // @Produce application/json
// @Param page query string false "page number" // @Param page query int true "page number" test
// @Tags Articles // @Tags Articles
// @Router /articles [get] // @Router /v1/articles [get]
// @Success 200 {object} domain.ArticleResponse // @Success 200 {object} domain.ArticleResponse
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (s *Handler) listArticles(c echo.Context) error { func (s *Handler) listArticles(c echo.Context) error {
resp := domain.ArticleResponse{ resp := domain.ArticleResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeArticleRead)
if err != nil {
return c.JSON(http.StatusUnauthorized, resp)
}
page, err := strconv.Atoi(c.QueryParam("page")) page, err := strconv.Atoi(c.QueryParam("page"))
if err != nil { if err != nil {
page = 0 page = 0
@ -32,107 +39,128 @@ func (s *Handler) listArticles(c echo.Context) error {
res, err := s.repo.Articles.ListByPage(c.Request().Context(), page, 25) res, err := s.repo.Articles.ListByPage(c.Request().Context(), page, 25)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) return c.JSON(http.StatusInternalServerError, resp)
} }
resp.Payload = services.ArticlesToDto(res) resp.Payload = dtoconv.ArticlesToDto(res)
resp.BaseResponse.IsError = false
return c.JSON(http.StatusOK, resp) return c.JSON(http.StatusOK, resp)
} }
// GetArticle // GetArticle
// @Summary Returns an article based on defined ID. // @Summary Returns an article based on defined ID.
// @Param ID path string true "int" // @Param id path string true "int"
// @Produce application/json // @Produce application/json
// @Tags Articles // @Tags Articles
// @Router /articles/{ID} [get] // @Router /v1/articles/{id} [get]
// @Success 200 {object} domain.ArticleResponse "OK" // @Success 200 {object} domain.ArticleResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (s *Handler) getArticle(c echo.Context) error { func (s *Handler) getArticle(c echo.Context) error {
p := domain.ArticleResponse{ p := domain.ArticleResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeArticleRead)
if err != nil {
return c.JSON(http.StatusUnauthorized, p)
}
id := c.Param("ID") id := c.Param("ID")
idNumber, err := strconv.Atoi(id) idNumber, err := strconv.Atoi(id)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) return c.JSON(http.StatusBadRequest, p)
} }
item, err := s.repo.Articles.GetById(c.Request().Context(), int64(idNumber)) item, err := s.repo.Articles.GetById(c.Request().Context(), int64(idNumber))
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err) return c.JSON(http.StatusBadRequest, p)
} }
var dtos []domain.ArticleDto var dtos []domain.ArticleDto
dtos = append(dtos, services.ArticleToDto(item)) dtos = append(dtos, dtoconv.ArticleToDto(item))
p.Payload = dtos p.Payload = dtos
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p) return c.JSON(http.StatusOK, p)
} }
// GetArticleDetails // GetArticleDetails
// @Summary Returns an article and source based on defined ID. // @Summary Returns an article and source based on defined ID.
// @Param ID path string true "int" // @Param id path string true "int"
// @Produce application/json // @Produce application/json
// @Tags Articles // @Tags Articles
// @Router /articles/{ID}/details [get] // @Router /v1/articles/{id}/details [get]
// @Success 200 {object} domain.ArticleDetailedResponse "OK" // @Success 200 {object} domain.ArticleDetailedResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (s *Handler) getArticleDetails(c echo.Context) error { func (s *Handler) getArticleDetails(c echo.Context) error {
p := domain.ArticleDetailedResponse{ p := domain.ArticleDetailedResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
Payload: domain.ArticleAndSourceModel{ Payload: domain.ArticleAndSourceModel{},
}
}, _, err := s.ValidateJwtToken(c, domain.ScopeArticleRead)
if err != nil {
return c.JSON(http.StatusUnauthorized, p)
} }
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) return c.JSON(http.StatusBadRequest, p)
} }
article, err := s.repo.Articles.GetById(c.Request().Context(), int64(id)) article, err := s.repo.Articles.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) return c.JSON(http.StatusBadRequest, p)
} }
source, err := s.repo.Sources.GetById(c.Request().Context(), article.SourceID) source, err := s.repo.Sources.GetById(c.Request().Context(), article.SourceID)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) return c.JSON(http.StatusBadRequest, p)
} }
p.Payload.Article = services.ArticleToDto(article) p.Payload.Article = dtoconv.ArticleToDto(article)
p.Payload.Source = services.SourceToDto(source) p.Payload.Source = dtoconv.SourceToDto(source)
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p) return c.JSON(http.StatusOK, p)
} }
// ListArticlesBySourceID // ListArticlesBySourceID
// @Summary Finds the articles based on the SourceID provided. Returns the top 25. // @Summary Finds the articles based on the SourceID provided. Returns the top 25.
// @Param id query string true "source id" // @Param id path string true "source id"
// @Param page query int false "Page to query" // @Param page query int false "Page to query"
// @Produce application/json // @Produce application/json
// @Tags Articles // @Tags Articles
// @Router /articles/by/sourceid [get] // @Router /v1/articles/by/source/{id} [get]
// @Success 200 {object} domain.ArticleResponse "OK" // @Success 200 {object} domain.ArticleResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (s *Handler) ListArticlesBySourceId(c echo.Context) error { func (s *Handler) ListArticlesBySourceId(c echo.Context) error {
p := domain.ArticleResponse{ p := domain.ArticleResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeArticleRead)
if err != nil {
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.QueryParam("id")) id, err := strconv.Atoi(c.QueryParam("id"))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) return c.JSON(http.StatusBadRequest, p)
} }
// if the page number is missing, default to 0 // if the page number is missing, default to 0
@ -143,9 +171,11 @@ func (s *Handler) ListArticlesBySourceId(c echo.Context) error {
items, err := s.repo.Articles.ListBySource(c.Request().Context(), _page, 25, id, "") items, err := s.repo.Articles.ListBySource(c.Request().Context(), _page, 25, id, "")
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err) return c.JSON(http.StatusBadRequest, p)
} }
p.Payload = services.ArticlesToDto(items) p.Payload = dtoconv.ArticlesToDto(items)
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p) return c.JSON(http.StatusOK, p)
} }

View File

@ -5,31 +5,40 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/dtoconv"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
// ListDiscordWebhooks // ListDiscordWebhooks
// @Summary Returns the top 100 // @Summary Returns the top 100
// @Produce application/json // @Produce application/json
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks [get] // @Router /v1/discord/webhooks [get]
// @Success 200 {object} domain.DiscordWebhookResponse // @Success 200 {object} domain.DiscordWebhookResponse
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) ListDiscordWebHooks(c echo.Context) error { func (s *Handler) ListDiscordWebHooks(c echo.Context) error {
p := domain.DiscordWebhookResponse{ p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead)
if err != nil {
return c.JSON(http.StatusUnauthorized, p)
}
res, err := s.repo.DiscordWebHooks.ListByServerName(c.Request().Context(), "") res, err := s.repo.DiscordWebHooks.ListByServerName(c.Request().Context(), "")
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err) return c.JSON(http.StatusInternalServerError, p)
} }
p.Payload = services.DiscordWebhooksToDto(res)
p.Payload = dtoconv.DiscordWebhooksToDto(res)
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p) return c.JSON(http.StatusOK, p)
} }
@ -37,30 +46,39 @@ func (s *Handler) ListDiscordWebHooks(c echo.Context) error {
// @Summary Returns the top 100 entries from the queue to be processed. // @Summary Returns the top 100 entries from the queue to be processed.
// @Produce application/json // @Produce application/json
// @Param id path int true "id" // @Param id path int true "id"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/{id} [get] // @Router /v1/discord/webhooks/{id} [get]
// @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) GetDiscordWebHooksById(c echo.Context) error { func (s *Handler) GetDiscordWebHooksById(c echo.Context) error {
p := domain.DiscordWebhookResponse{ p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead)
if err != nil {
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) return c.JSON(http.StatusBadRequest, p)
} }
res, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) res, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) return c.JSON(http.StatusInternalServerError, p)
} }
var dtos []domain.DiscordWebHookDto var dtos []domain.DiscordWebHookDto
dtos = append(dtos, services.DiscordWebhookToDto(res)) dtos = append(dtos, dtoconv.DiscordWebhookToDto(res))
p.Payload = dtos p.Payload = dtos
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p) return c.JSON(http.StatusOK, p)
} }
@ -69,34 +87,46 @@ func (s *Handler) GetDiscordWebHooksById(c echo.Context) error {
// @Produce application/json // @Produce application/json
// @Param server query string true "Fancy Server" // @Param server query string true "Fancy Server"
// @Param channel query string true "memes" // @Param channel query string true "memes"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/by/serverAndChannel [get] // @Router /v1/discord/webhooks/by/serverAndChannel [get]
// @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error { func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error {
p := domain.DiscordWebhookResponse{ p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebhookRead)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
_server := c.QueryParam("server") _server := c.QueryParam("server")
if _server == "" { if _server == "" {
s.WriteMessage(c, "server was not defined", http.StatusBadRequest) p.BaseResponse.Message = "server was not defined"
return c.JSON(http.StatusBadRequest, p)
} }
_channel := c.QueryParam("channel") _channel := c.QueryParam("channel")
if _channel == "" { if _channel == "" {
s.WriteMessage(c, "channel was not defined", http.StatusBadRequest) p.BaseResponse.Message = "channel was not defined"
return c.JSON(http.StatusBadRequest, p)
} }
res, err := s.repo.DiscordWebHooks.ListByServerAndChannel(c.Request().Context(), _server, _channel) res, err := s.repo.DiscordWebHooks.ListByServerAndChannel(c.Request().Context(), _server, _channel)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
p.Payload = services.DiscordWebhooksToDto(res) p.Payload = dtoconv.DiscordWebhooksToDto(res)
p.IsError = false
return c.JSON(http.StatusOK, p) return c.JSON(http.StatusOK, p)
} }
@ -105,201 +135,284 @@ func (s *Handler) GetDiscordWebHooksByServerAndChannel(c echo.Context) error {
// @Param url query string true "url" // @Param url query string true "url"
// @Param server query string true "Server name" // @Param server query string true "Server name"
// @Param channel query string true "Channel name" // @Param channel query string true "Channel name"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/new [post] // @Router /v1/discord/webhooks/new [post]
// @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) NewDiscordWebHook(c echo.Context) error { func (s *Handler) NewDiscordWebHook(c echo.Context) error {
p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
}
token, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
_url := c.QueryParam("url") _url := c.QueryParam("url")
_server := c.QueryParam("server") _server := c.QueryParam("server")
_channel := c.QueryParam("channel") _channel := c.QueryParam("channel")
if _url == "" { if _url == "" {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.Message = "url is missing a value"
Message: "url is missing a value", return c.JSON(http.StatusBadRequest, p)
})
} }
if !strings.Contains(_url, "discord.com/api/webhooks") { if !strings.Contains(_url, "discord.com/api/webhooks") {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.Message = "invalid url"
Message: "invalid url", return c.JSON(http.StatusBadRequest, p)
})
} }
if _server == "" { if _server == "" {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.Message = "server is missing"
Message: "server is missing", return c.JSON(http.StatusBadRequest, p)
})
} }
if _channel == "" { if _channel == "" {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.Message = "channel is missing"
Message: "channel is missing", return c.JSON(http.StatusBadRequest, p)
})
} }
rows, err := s.repo.DiscordWebHooks.Create(c.Request().Context(), _url, _server, _channel, true) user, err := s.repo.Users.GetUser(c.Request().Context(), token.UserName)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
rows, err := s.repo.DiscordWebHooks.Create(c.Request().Context(), user.ID, _url, _server, _channel, true)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
if rows != 1 { if rows != 1 {
s.WriteMessage(c, "data was not written to database", http.StatusInternalServerError) p.Message = "data was not written to database"
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.DiscordWebHooks.GetByUrl(c.Request().Context(), _url) item, err := s.repo.DiscordWebHooks.GetByUrl(c.Request().Context(), _url)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dtos []domain.DiscordWebHookDto var dtos []domain.DiscordWebHookDto
dtos = append(dtos, services.DiscordWebhookToDto(item)) dtos = append(dtos, dtoconv.DiscordWebhookToDto(item))
return c.JSON(http.StatusOK, domain.DiscordWebhookResponse{ p.Payload = dtos
BaseResponse: domain.BaseResponse{ p.IsError = false
Message: ResponseMessageSuccess, return c.JSON(http.StatusOK, p)
},
Payload: dtos,
})
} }
// DisableDiscordWebHooks // DisableDiscordWebHooks
// @Summary Disables a Webhook from being used. // @Summary Disables a Webhook from being used.
// @Param id path int true "id" // @Param id path int true "id"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/{ID}/disable [post] // @Router /v1/discord/webhooks/{id}/disable [post]
// @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) disableDiscordWebHook(c echo.Context) error { func (s *Handler) disableDiscordWebHook(c echo.Context) error {
p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
}
_, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusBadRequest, p)
})
} }
// Check to make sure we can find the record // Check to make sure we can find the record
_, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
if record.UserID != s.GetUserIdFromJwtToken(c) {
p.BaseResponse.Message = ErrYouDontOwnTheRecord
return c.JSON(http.StatusBadRequest, p)
} }
// flip the it // flip the it
updated, err := s.repo.DiscordWebHooks.Disable(c.Request().Context(), int64(id)) updated, err := s.repo.DiscordWebHooks.Disable(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
// make sure we got a row updated // make sure we got a row updated
if updated != 1 { if updated != 1 {
s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) p.BaseResponse.Message = "unexpected number of updates found"
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dtos []domain.DiscordWebHookDto var dtos []domain.DiscordWebHookDto
dtos = append(dtos, services.DiscordWebhookToDto(item)) dtos = append(dtos, dtoconv.DiscordWebhookToDto(item))
return c.JSON(http.StatusOK, domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{ p.Payload = dtos
Message: ResponseMessageSuccess, p.IsError = false
}, return c.JSON(http.StatusOK, p)
Payload: dtos,
})
} }
// EnableDiscordWebHook // EnableDiscordWebHook
// @Summary Enables a source to continue processing. // @Summary Enables a source to continue processing.
// @Param id path int true "id" // @Param id path int true "id"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/{ID}/enable [post] // @Router /v1/discord/webhooks/{id}/enable [post]
// @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) enableDiscordWebHook(c echo.Context) error { func (s *Handler) enableDiscordWebHook(c echo.Context) error {
p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
}
_, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
// Check to make sure we can find the record // Check to make sure we can find the record
_, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
if record.UserID != s.GetUserIdFromJwtToken(c) {
p.BaseResponse.Message = ErrYouDontOwnTheRecord
return c.JSON(http.StatusBadRequest, p)
} }
updated, err := s.repo.DiscordWebHooks.Enable(c.Request().Context(), int64(id)) updated, err := s.repo.DiscordWebHooks.Enable(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
if updated != 1 { if updated != 1 {
s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) p.BaseResponse.Message = ErrFailedToUpdateRecord
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dtos []domain.DiscordWebHookDto var dtos []domain.DiscordWebHookDto
dtos = append(dtos, services.DiscordWebhookToDto(item)) dtos = append(dtos, dtoconv.DiscordWebhookToDto(item))
return c.JSON(http.StatusOK, domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{ p.Payload = dtos
Message: ResponseMessageSuccess, p.IsError = false
}, return c.JSON(http.StatusOK, p)
Payload: dtos,
})
} }
// DeleteDiscordWebHook // DeleteDiscordWebHook
// @Summary Deletes a record by ID. // @Summary Deletes a record by ID.
// @Param id path string true "id" // @Param id path string true "id"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/{ID} [delete] // @Router /v1/discord/webhooks/{id} [delete]
// @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.DiscordWebhookResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.DiscordWebhookResponse
// @Security Bearer
func (s *Handler) deleteDiscordWebHook(c echo.Context) error { func (s *Handler) deleteDiscordWebHook(c echo.Context) error {
p := domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
}
_, err := s.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, err.Error()) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
// Check to make sure we can find the record // Check to make sure we can find the record
_, err = s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) record, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error()) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
if record.UserID != s.GetUserIdFromJwtToken(c) {
p.BaseResponse.Message = ErrYouDontOwnTheRecord
return c.JSON(http.StatusBadRequest, p)
} }
// Soft delete the record // Soft delete the record
updated, err := s.repo.DiscordWebHooks.SoftDelete(c.Request().Context(), int64(id)) updated, err := s.repo.DiscordWebHooks.SoftDelete(c.Request().Context(), int64(id))
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error()) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
if updated != 1 { if updated != 1 {
s.WriteMessage(c, "unexpected number of updates found", http.StatusInternalServerError) p.BaseResponse.Message = ErrFailedToUpdateRecord
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id)) item, err := s.repo.DiscordWebHooks.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dtos []domain.DiscordWebHookDto var dtos []domain.DiscordWebHookDto
dtos = append(dtos, services.DiscordWebhookToDto(item)) dtos = append(dtos, dtoconv.DiscordWebhookToDto(item))
return c.JSON(http.StatusOK, domain.DiscordWebhookResponse{
BaseResponse: domain.BaseResponse{ p.Payload = dtos
Message: ResponseMessageSuccess, p.BaseResponse.IsError = false
}, return c.JSON(http.StatusOK, p)
Payload: dtos,
})
} }
// UpdateDiscordWebHook // UpdateDiscordWebHook
// @Summary Updates a valid discord webhook ID based on the body given. // @Summary Updates a valid discord webhook ID based on the body given.
// @Param id path string true "id" // @Param id path string true "id"
// @Tags Discord, Webhook // @Tags DiscordWebhook
// @Router /discord/webhooks/{id} [patch] // @Router /v1/discord/webhooks/{id} [patch]
// @Success 200 {object} domain.DiscordWebhookResponse "OK" // @Success 200 {object} domain.DiscordWebhookResponse "OK"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse

View File

@ -3,21 +3,21 @@ package v1
import ( import (
"context" "context"
"database/sql" "database/sql"
"errors"
"github.com/golang-jwt/jwt/v5"
echojwt "github.com/labstack/echo-jwt/v4"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
_ "github.com/lib/pq" "github.com/labstack/echo/v4/middleware"
swagger "github.com/swaggo/echo-swagger" swagger "github.com/swaggo/echo-swagger"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" _ "git.jamestombleson.com/jtom38/newsbot-api/docs"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/dto"
) )
type Handler struct { type Handler struct {
Router *echo.Echo Router *echo.Echo
Db *database.Queries //Db *database.Queries
dto *dto.DtoClient
config services.Configs config services.Configs
repo services.RepositoryService repo services.RepositoryService
} }
@ -26,8 +26,13 @@ const (
ErrParameterIdMissing = "The requested parameter ID was not found." ErrParameterIdMissing = "The requested parameter ID was not found."
ErrParameterMissing = "The requested parameter was not found found:" ErrParameterMissing = "The requested parameter was not found found:"
ErrUnableToParseId = "Unable to parse the requested ID" ErrUnableToParseId = "Unable to parse the requested ID"
ErrRecordMissing = "The requested record was not found" ErrRecordMissing = "The requested record was not found"
ErrFailedToCreateRecord = "The record was not created due to a database problem" ErrFailedToCreateRecord = "The record was not created due to a database problem"
ErrFailedToUpdateRecord = "The requested record was not updated due to a database problem"
ErrUserUnknown = "User is unknown"
ErrYouDontOwnTheRecord = "The record requested does not belong to you"
ResponseMessageSuccess = "Success" ResponseMessageSuccess = "Success"
) )
@ -39,23 +44,32 @@ var (
ErrUnableToConvertToJson string = "Unable to convert to json" ErrUnableToConvertToJson string = "Unable to convert to json"
) )
func NewServer(ctx context.Context, db *database.Queries, configs services.Configs, conn *sql.DB) *Handler { func NewServer(ctx context.Context, configs services.Configs, conn *sql.DB) *Handler {
s := &Handler{ s := &Handler{
Db: db,
dto: dto.NewDtoClient(db),
config: configs, config: configs,
repo: services.NewRepositoryService(conn), repo: services.NewRepositoryService(conn),
} }
jwtConfig := echojwt.Config{
NewClaimsFunc: func(c echo.Context) jwt.Claims {
return new(JwtToken)
},
SigningKey: []byte(configs.JwtSecret),
}
router := echo.New() router := echo.New()
router.Pre(middleware.RemoveTrailingSlash())
router.Pre(middleware.Logger())
router.Pre(middleware.Recover())
router.GET("/swagger/*", swagger.WrapHandler) router.GET("/swagger/*", swagger.WrapHandler)
v1 := router.Group("/api/v1") v1 := router.Group("/api/v1")
articles := v1.Group("/articles") articles := v1.Group("/articles")
articles.GET("/", s.listArticles) articles.Use(echojwt.WithConfig(jwtConfig))
articles.GET("/:id", s.getArticle) articles.GET("", s.listArticles)
articles.GET("/:id/details", s.getArticleDetails) articles.GET(":id", s.getArticle)
articles.GET("/by/source/:id", s.ListArticlesBySourceId) articles.GET(":id/details", s.getArticleDetails)
articles.GET("by/source/:id", s.ListArticlesBySourceId)
//dwh := v1.Group("/discord/webhooks") //dwh := v1.Group("/discord/webhooks")
//dwh.GET("/", s.ListDiscordWebHooks) //dwh.GET("/", s.ListDiscordWebHooks)
@ -73,46 +87,111 @@ func NewServer(ctx context.Context, db *database.Queries, configs services.Confi
//settings.GET("/", s.getSettings) //settings.GET("/", s.getSettings)
sources := v1.Group("/sources") sources := v1.Group("/sources")
sources.GET("/", s.listSources) sources.Use(echojwt.WithConfig(jwtConfig))
sources.GET("", s.listSources)
sources.GET("/by/source", s.listSourcesBySource) sources.GET("/by/source", s.listSourcesBySource)
sources.GET("/by/sourceAndName", s.GetSourceBySourceAndName) sources.GET("/by/sourceAndName", s.GetSourceBySourceAndName)
//sources.POST("/new/reddit", s.newRedditSource) //sources.POST("/new/reddit", s.newRedditSource)
//sources.POST("/new/youtube", s.newYoutubeSource) //sources.POST("/new/youtube", s.newYoutubeSource)
//sources.POST("/new/twitch", s.newTwitchSource) //sources.POST("/new/twitch", s.newTwitchSource)
sources.POST("/new/rss", s.newRssSource) sources.POST("/new/rss", s.newRssSource)
sources.GET("/:ID/", s.getSource) sources.GET("/:id", s.getSource)
sources.DELETE("/:ID/", s.deleteSources) sources.DELETE("/:id", s.deleteSources)
sources.POST("/:ID/disable", s.disableSource) sources.POST("/:id/disable", s.disableSource)
sources.POST("/:ID/enable", s.enableSource) sources.POST("/:id/enable", s.enableSource)
subs := v1.Group("/subscriptions") users := v1.Group("/users")
subs.GET("/", s.ListSubscriptions) users.POST("/login", s.AuthLogin)
subs.GET("/details", s.ListSubscriptionDetails) users.POST("/register", s.AuthRegister)
subs.GET("/by/discordId", s.GetSubscriptionsByDiscordId) users.Use(echojwt.WithConfig(jwtConfig))
subs.GET("/by/sourceId", s.GetSubscriptionsBySourceId) users.POST("/scopes/add", s.AddScopes)
subs.POST("/discord/webhook/new", s.newDiscordWebHookSubscription) users.POST("/scopes/remove", s.RemoveScopes)
subs.DELETE("/discord/webhook/delete", s.DeleteDiscordWebHookSubscription) users.POST("/refresh/token", s.RefreshJwtToken)
users.POST("/refresh/sessionToken", s.NewSessionToken)
s.Router = router
return s return s
} }
type ApiStatusModel struct { //type ApiStatusModel struct {
StatusCode int `json:"status"` // StatusCode int `json:"status"`
Message string `json:"message"` // Message string `json:"message"`
//}
//type ApiError struct {
// *ApiStatusModel
//}
//func (s *Handler) WriteError(c echo.Context, errMessage error, HttpStatusCode int) error {
// return c.JSON(HttpStatusCode, domain.BaseResponse{
// Message: errMessage.Error(),
// IsError: true,
// })
//}
//func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) error {
// return c.JSON(HttpStatusCode, domain.BaseResponse{
// Message: msg,
// })
//}
//func (s *Handler) InternalServerErrorResponse(c echo.Context, msg string) error {
// return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
// Message: msg,
// })
//}
//func (s *Handler) UnauthorizedResponse(c echo.Context, msg string) error {
// return c.JSON(http.StatusUnauthorized, domain.BaseResponse{
// Message: msg,
// })
//}
// If the token is not valid then an json error will be returned.
// If the token has the wrong scope, a json error will be returned.
// If the token passes all the checks, it is valid and is returned back to the caller.
func (s *Handler) ValidateJwtToken(c echo.Context, requiredScope string) (JwtToken, error) {
token, err := s.getJwtTokenFromContext(c)
if err != nil {
return JwtToken{}, errors.New(ErrJwtMissing)
}
err = token.hasExpired()
if err != nil {
return JwtToken{}, errors.New(ErrJwtExpired)
}
err = token.hasScope(requiredScope)
if err != nil {
return JwtToken{}, errors.New(ErrJwtScopeMissing)
}
if token.Iss != s.config.ServerAddress {
return JwtToken{}, errors.New(ErrJwtInvalidIssuer)
}
// If you are the built in admin account, skip the username and session token check
if token.UserName == "admin" {
return token, nil
}
user, err := s.repo.Users.GetUser(c.Request().Context(), token.UserName)
if err != nil {
return JwtToken{}, errors.New("user record not found")
}
if user.SessionToken != token.SessionToken {
return JwtToken{}, errors.New("invalid session token")
}
return token, nil
} }
type ApiError struct { func (s *Handler) GetUserIdFromJwtToken(c echo.Context) int64 {
*ApiStatusModel token, err := s.getJwtTokenFromContext(c)
} if err != nil {
return -1
}
func (s *Handler) WriteError(c echo.Context, errMessage error, HttpStatusCode int) error { return token.GetUserId()
return c.JSON(HttpStatusCode, domain.BaseResponse{
Message: errMessage.Error(),
})
}
func (s *Handler) WriteMessage(c echo.Context, msg string, HttpStatusCode int) error {
return c.JSON(HttpStatusCode, domain.BaseResponse{
Message: msg,
})
} }

130
internal/handler/v1/jwt.go Normal file
View File

@ -0,0 +1,130 @@
package v1
import (
"errors"
"strings"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"github.com/golang-jwt/jwt/v5"
"github.com/labstack/echo/v4"
)
const (
ErrJwtMissing = "auth token is missing"
ErrJwtClaimsMissing = "claims missing on token"
ErrJwtExpired = "auth token has expired"
ErrJwtScopeMissing = "required scope is missing"
ErrJwtInvalidIssuer = "incorrect server issued the token"
)
type JwtToken struct {
Exp time.Time `json:"exp"`
Iss string `json:"iss"`
Authorized bool `json:"authorized"`
UserName string `json:"username"`
UserId int64 `json:"userId"`
Scopes []string `json:"scopes"`
SessionToken string `json:"sessionToken"`
jwt.RegisteredClaims
}
func (j JwtToken) IsValid(scope string) error {
err := j.hasExpired()
if err != nil {
return err
}
// Check to see if they have the scope to do anything
// if they do, let them pass
err = j.hasScope(domain.ScopeAll)
if err == nil {
return nil
}
err = j.hasScope(scope)
if err != nil {
return err
}
return nil
}
func (j JwtToken) GetUsername() string {
return j.UserName
}
func (j JwtToken) GetUserId() int64 {
return j.UserId
}
func (j JwtToken) hasExpired() error {
// Check to see if the token has expired
//hasExpired := j.Exp.Compare(time.Now())
hasExpired := time.Now().Compare(j.Exp)
if hasExpired == 1 {
return errors.New(ErrJwtExpired)
}
return nil
}
// This will check the users token to make sure they have the correct scope to access the handler.
// It will evaluate if you have the admin scope or the required scope for the handler.
func (j JwtToken) hasScope(scope string) error {
// they have the scope to access everything, so let them pass.
userScopes := strings.Join(j.Scopes, "")
if strings.Contains(domain.ScopeAll, userScopes) {
return nil
}
if strings.Contains(userScopes, scope) {
return nil
}
return errors.New(ErrJwtScopeMissing)
}
func (h *Handler) generateJwt(username, issuer, sessionToken string, userScopes []string, userId int64) (string, error) {
return h.generateJwtWithExp(username, issuer, sessionToken, userScopes, userId, time.Now().Add(10*time.Minute))
}
func (h *Handler) generateJwtWithExp(username, issuer, sessionToken string, userScopes []string, userId int64, expiresAt time.Time) (string, error) {
secret := []byte(h.config.JwtSecret)
// Anyone who wants to decrypt the key needs to use the same method
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = expiresAt
claims["authorized"] = true
claims["username"] = username
claims["iss"] = issuer
claims["userId"] = userId
claims["sessionToken"] = sessionToken
var scopes []string
scopes = append(scopes, userScopes...)
claims["scopes"] = scopes
tokenString, err := token.SignedString(secret)
if err != nil {
return "", err
}
return tokenString, nil
}
func (h *Handler) getJwtTokenFromContext(c echo.Context) (JwtToken, error) {
// Make sure that the request came with a jwtToken
token, ok := c.Get("user").(*jwt.Token)
if !ok {
return JwtToken{}, errors.New(ErrJwtMissing)
}
// Generate the claims from the token
claims, ok := token.Claims.(*JwtToken)
if !ok {
return JwtToken{}, errors.New(ErrJwtClaimsMissing)
}
return *claims, nil
}

View File

@ -1,40 +0,0 @@
package v1
import (
"net/http"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
"github.com/labstack/echo/v4"
)
type ListDiscordWebHooksQueueResults struct {
ApiStatusModel
Payload []models.DiscordQueueDetailsDto `json:"payload"`
}
// GetDiscordQueue
// @Summary Returns the top 100 entries from the queue to be processed.
// @Produce application/json
// @Tags Queue
// @Router /queue/discord/webhooks [get]
// @Success 200 {object} ListDiscordWebHooksQueueResults "ok"
func (s *Handler) ListDiscordWebhookQueue(c echo.Context) error {
p := ListDiscordWebHooksQueueResults{
ApiStatusModel: ApiStatusModel{
Message: "OK",
StatusCode: http.StatusOK,
},
}
// Get the raw resp from sql
res, err := s.dto.ListDiscordWebhookQueueDetails(c.Request().Context(), 50)
if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{
Message: err.Error(),
})
}
p.Payload = res
return c.JSON(http.StatusOK, p)
}

View File

@ -1,39 +0,0 @@
package v1
import (
"encoding/json"
"net/http"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
// GetSettings
// @Summary Returns a object based on the Key that was given.
// @Param key path string true "Settings Key value"
// @Produce application/json
// @Tags Settings
// @Router /settings/{key} [get]
func (s *Handler) getSettings(c echo.Context) error {
id := c.Param("ID")
uuid, err := uuid.Parse(id)
if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{
Message: err.Error(),
})
}
res, err := s.Db.GetSourceByID(c.Request().Context(), uuid)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
bResult, err := json.Marshal(res)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
return c.JSON(http.StatusOK, bResult)
}

View File

@ -1,46 +1,40 @@
package v1 package v1
import ( import (
"context"
"encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings" "strings"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/dtoconv"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"github.com/google/uuid"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
type ListSources struct {
ApiStatusModel
Payload []models.SourceDto `json:"payload"`
}
type GetSource struct {
ApiStatusModel
Payload models.SourceDto `json:"payload"`
}
// ListSources // ListSources
// @Summary Lists the top 50 records // @Summary Lists the top 50 records
// @Param page query string false "page number" // @Param page query string false "page number"
// @Produce application/json // @Produce application/json
// @Tags Source // @Tags Source
// @Router /sources [get] // @Router /v1/sources [get]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse "Unable to reach SQL or Data problems" // @Failure 400 {object} domain.SourcesResponse "Unable to reach SQL or Data problems"
// @Security Bearer
func (s *Handler) listSources(c echo.Context) error { func (s *Handler) listSources(c echo.Context) error {
resp := domain.SourcesResponse { p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeSourceRead)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
page, err := strconv.Atoi(c.QueryParam("page")) page, err := strconv.Atoi(c.QueryParam("page"))
if err != nil { if err != nil {
page = 0 page = 0
@ -49,11 +43,13 @@ func (s *Handler) listSources(c echo.Context) error {
// Default way of showing all sources // Default way of showing all sources
items, err := s.repo.Sources.List(c.Request().Context(), page, 25) items, err := s.repo.Sources.List(c.Request().Context(), page, 25)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
resp.Payload = services.SourcesToDto(items) p.Payload = dtoconv.SourcesToDto(items)
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// ListSourcesBySource // ListSourcesBySource
@ -62,20 +58,29 @@ func (s *Handler) listSources(c echo.Context) error {
// @Param page query string false "page number" // @Param page query string false "page number"
// @Produce application/json // @Produce application/json
// @Tags Source // @Tags Source
// @Router /sources/by/source [get] // @Router /v1/sources/by/source [get]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) listSourcesBySource(c echo.Context) error { func (s *Handler) listSourcesBySource(c echo.Context) error {
resp := domain.SourcesResponse{ p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeSourceRead)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
source := c.QueryParam("source") source := c.QueryParam("source")
if source == "" { if source == "" {
s.WriteMessage(c, fmt.Sprintf("%s source", ErrParameterMissing), http.StatusBadRequest) p.BaseResponse.Message = fmt.Sprintf("%s source", ErrParameterMissing)
return c.JSON(http.StatusBadRequest, p)
} }
page, err := strconv.Atoi(c.QueryParam("page")) page, err := strconv.Atoi(c.QueryParam("page"))
@ -86,47 +91,55 @@ func (s *Handler) listSourcesBySource(c echo.Context) error {
// Shows the list by Sources.source // Shows the list by Sources.source
items, err := s.repo.Sources.ListBySource(c.Request().Context(), page, 25, source) items, err := s.repo.Sources.ListBySource(c.Request().Context(), page, 25, source)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusInternalServerError, p)
})
} }
resp.Payload = services.SourcesToDto(items) p.Payload = dtoconv.SourcesToDto(items)
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// GetSource // GetSource
// @Summary Returns a single entity by ID // @Summary Returns a single entity by ID
// @Param id path int true "uuid" // @Param id path int true "id"
// @Produce application/json // @Produce application/json
// @Tags Source // @Tags Source
// @Router /sources/{id} [get] // @Router /v1/sources/{id} [get]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) getSource(c echo.Context) error { func (s *Handler) getSource(c echo.Context) error {
resp := domain.SourcesResponse{ p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
}, },
} }
id, err := strconv.Atoi(c.Param("ID")) _, err := s.ValidateJwtToken(c, domain.ScopeSourceRead)
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: ErrUnableToParseId, return c.JSON(http.StatusUnauthorized, p)
}) }
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dto []domain.SourceDto var dto []domain.SourceDto
dto = append(dto, services.SourceToDto(item)) dto = append(dto, dtoconv.SourceToDto(item))
resp.Payload = dto p.Payload = dto
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// GetSourceByNameAndSource // GetSourceByNameAndSource
@ -135,34 +148,43 @@ func (s *Handler) getSource(c echo.Context) error {
// @Param source query string true "reddit" // @Param source query string true "reddit"
// @Produce application/json // @Produce application/json
// @Tags Source // @Tags Source
// @Router /sources/by/sourceAndName [get] // @Router /v1/sources/by/sourceAndName [get]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (s *Handler) GetSourceBySourceAndName(c echo.Context) error { func (s *Handler) GetSourceBySourceAndName(c echo.Context) error {
resp := domain.SourcesResponse{ p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
var param domain.GetSourceBySourceAndNameParamRequest _, err := s.ValidateJwtToken(c, domain.ScopeSourceRead)
err := c.Bind(&param)
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusUnauthorized, p)
}) }
var param domain.GetSourceBySourceAndNameParamRequest
err = c.Bind(&param)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), param.Source, param.Name) item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), param.Source, param.Name)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error()) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dto []domain.SourceDto var dto []domain.SourceDto
dto = append(dto, services.SourceToDto(item)) dto = append(dto, dtoconv.SourceToDto(item))
resp.Payload = dto p.Payload = dto
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// NewRedditSource // NewRedditSource
@ -170,55 +192,73 @@ func (s *Handler) GetSourceBySourceAndName(c echo.Context) error {
// @Param name query string true "name" // @Param name query string true "name"
// @Param url query string true "url" // @Param url query string true "url"
// @Tags Source // @Tags Source
// @Router /sources/new/reddit [post] // @Router /v1/sources/new/reddit [post]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) newRedditSource(c echo.Context) error { func (s *Handler) newRedditSource(c echo.Context) error {
resp := domain.SourcesResponse{ p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
var param domain.NewSourceParamRequest _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate)
err := c.Bind(&param)
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusUnauthorized, p)
})
} }
var param domain.NewSourceParamRequest
err = c.Bind(&param)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
if param.Url == "" { if param.Url == "" {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = "url is missing"
Message: "Url is missing a value", return c.JSON(http.StatusBadRequest, p)
})
} }
if !strings.Contains(param.Url, "reddit.com") { if !strings.Contains(param.Url, "reddit.com") {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = "invalid url"
Message: "Invalid URL given", return c.JSON(http.StatusBadRequest, p)
}) }
// Check to see if we already have this record, if we do, return it.
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorReddit, param.Name)
if err == nil {
var dto []domain.SourceDto
dto = append(dto, dtoconv.SourceToDto(item))
p.Payload = dto
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
tags := fmt.Sprintf("twitch, %v, %s", param.Name, param.Tags) tags := fmt.Sprintf("twitch, %v, %s", param.Name, param.Tags)
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorReddit, param.Name, param.Url, tags, true) rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorReddit, param.Name, param.Url, tags, true)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
if rows != 1 { if rows != 1 {
s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) p.BaseResponse.Message = ErrFailedToCreateRecord
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorReddit, param.Name) item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorReddit, param.Name)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dto []domain.SourceDto var dto []domain.SourceDto
dto = append(dto, services.SourceToDto(item)) dto = append(dto, dtoconv.SourceToDto(item))
resp.Payload = dto p.Payload = dto
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// NewYoutubeSource // NewYoutubeSource
@ -226,288 +266,388 @@ func (s *Handler) newRedditSource(c echo.Context) error {
// @Param name query string true "name" // @Param name query string true "name"
// @Param url query string true "url" // @Param url query string true "url"
// @Tags Source // @Tags Source
// @Router /sources/new/youtube [post] // @Router /v1/sources/new/youtube [post]
// @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) newYoutubeSource(c echo.Context) error { func (s *Handler) newYoutubeSource(c echo.Context) error {
var param domain.NewSourceParamRequest p := domain.SourcesResponse{
err := c.Bind(&param) BaseResponse: domain.BaseResponse{
if err != nil { Message: ResponseMessageSuccess,
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ },
Message: err.Error(),
})
} }
//query := r.URL.Query() // Validate the jwt
//_name := query["name"][0] _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate)
//_url := query["url"][0] if err != nil {
////_tags := query["tags"][0] p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
var param domain.NewSourceParamRequest
err = c.Bind(&param)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
if param.Url == "" { if param.Url == "" {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = "url is missing a value"
Message: "url is missing a value", return c.JSON(http.StatusBadRequest, p)
})
} }
if !strings.Contains(param.Url, "youtube.com") { if !strings.Contains(param.Url, "youtube.com") {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = "invalid url"
Message: "Invalid URL", return c.JSON(http.StatusBadRequest, p)
})
} }
/* item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name)
if _tags == "" { if err == nil {
tags = fmt.Sprintf("twitch, %v", _name) var dto []domain.SourceDto
} else { dto = append(dto, dtoconv.SourceToDto(item))
p.Payload = dto
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
*/
tags := fmt.Sprintf("twitch, %v", param.Name) tags := fmt.Sprintf("twitch, %v", param.Name)
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorYoutube, param.Name, param.Url, tags, true)
params := database.CreateSourceParams{
ID: uuid.New(),
Site: "youtube",
Name: param.Name,
Source: "youtube",
Type: "feed",
Enabled: true,
Url: param.Url,
Tags: tags,
}
err = s.Db.CreateSource(context.Background(), params)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error()) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
bJson, err := json.Marshal(&params) if rows != 1 {
p.BaseResponse.Message = ErrFailedToCreateRecord
return c.JSON(http.StatusInternalServerError, p)
}
item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorYoutube, param.Name)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusInternalServerError, p)
})
} }
return c.JSON(http.StatusOK, bJson) var dto []domain.SourceDto
dto = append(dto, dtoconv.SourceToDto(item))
p.Payload = dto
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// NewTwitchSource // NewTwitchSource
// @Summary Creates a new twitch source to monitor. // @Summary Creates a new twitch source to monitor.
// @Param name query string true "name" // @Param name query string true "name"
// @Tags Source // @Tags Source
// @Router /sources/new/twitch [post] // @Router /v1/sources/new/twitch [post]
// @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) newTwitchSource(c echo.Context) error { func (s *Handler) newTwitchSource(c echo.Context) error {
var param domain.NewSourceParamRequest p := domain.SourcesResponse{
err := c.Bind(&param) BaseResponse: domain.BaseResponse{
if err != nil { Message: ResponseMessageSuccess,
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ IsError: true,
Message: err.Error(), },
})
} }
//query := r.URL.Query() _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate)
//_name := query["name"][0] if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
var param domain.NewSourceParamRequest
err = c.Bind(&param)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
tags := fmt.Sprintf("twitch, %v", param.Name) tags := fmt.Sprintf("twitch, %v", param.Name)
_url := fmt.Sprintf("https://twitch.tv/%v", param.Name) url := fmt.Sprintf("https://twitch.tv/%v", param.Name)
params := database.CreateSourceParams{ // Check if the record already exists
ID: uuid.New(), item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name)
Site: "twitch", if err == nil {
Name: param.Name, var dto []domain.SourceDto
Source: "twitch", dto = append(dto, dtoconv.SourceToDto(item))
Type: "api", p.Payload = dto
Enabled: true, p.BaseResponse.IsError = false
Url: _url, return c.JSON(http.StatusOK, p)
Tags: tags,
} }
err = s.Db.CreateSource(c.Request().Context(), params)
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorTwitch, param.Name, url, tags, true)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusInternalServerError, p)
})
} }
bJson, err := json.Marshal(&params) if rows != 1 {
if err != nil { p.BaseResponse.Message = ErrFailedToCreateRecord
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ return c.JSON(http.StatusInternalServerError, p)
Message: err.Error(),
})
} }
return c.JSON(http.StatusOK, bJson) item, _ = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorTwitch, param.Name)
var dto []domain.SourceDto
dto = append(dto, dtoconv.SourceToDto(item))
p.Payload = dto
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// NewRssSource // NewRssSource
// @Summary Creates a new rss source to monitor. // @Summary Creates a new rss source to monitor.
// @Param name query string true "Site Name" // @Param request body domain.NewSourceParamRequest true "body"
// @Param url query string true "RSS Url"
// @Tags Source // @Tags Source
// @Router /sources/new/rss [post] // @Router /v1/sources/new/rss [post]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) newRssSource(c echo.Context) error { func (s *Handler) newRssSource(c echo.Context) error {
resp := domain.SourcesResponse{ p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
var param domain.NewSourceParamRequest _, err := s.ValidateJwtToken(c, domain.ScopeSourceCreate)
err := c.Bind(&param)
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusUnauthorized, p)
}) }
param := domain.NewSourceParamRequest{}
err = c.Bind(&param)
//err = (&echo.DefaultBinder{}).BindBody(c, &param)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
if param.Url == "" { if param.Url == "" {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: "Url is missing a value", return c.JSON(http.StatusBadRequest, p)
})
} }
// Check if the record already exists
blankRecord := entity.SourceEntity{}
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorRss, param.Name)
if err != nil {
p.IsError = true
return c.JSON(http.StatusInternalServerError, p)
}
// No record was found, but no error returned
if item != blankRecord {
var dto []domain.SourceDto
dto = append(dto, dtoconv.SourceToDto(item))
p.Payload = dto
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
}
//if err == nil {
// var dto []domain.SourceDto
// dto = append(dto, dtoconv.SourceToDto(item))
// p.Payload = dto
// p.BaseResponse.IsError = false
// return c.JSON(http.StatusOK, p)
//}
tags := fmt.Sprintf("rss, %v, %s", param.Name, param.Tags) tags := fmt.Sprintf("rss, %v, %s", param.Name, param.Tags)
rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorRss, param.Name, param.Url, tags, true) rows, err := s.repo.Sources.Create(c.Request().Context(), domain.SourceCollectorRss, param.Name, param.Url, tags, true)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
if rows != 1 { if rows != 1 {
s.WriteMessage(c, ErrFailedToCreateRecord, http.StatusInternalServerError) p.BaseResponse.Message = ErrFailedToCreateRecord
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorRss, param.Name) item, err = s.repo.Sources.GetBySourceAndName(c.Request().Context(), domain.SourceCollectorRss, param.Name)
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dto []domain.SourceDto var dto []domain.SourceDto
dto = append(dto, services.SourceToDto(item)) dto = append(dto, dtoconv.SourceToDto(item))
resp.Payload = dto p.Payload = dto
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// DeleteSource // DeleteSource
// @Summary Marks a source as deleted based on its ID value. // @Summary Marks a source as deleted based on its ID value.
// @Param id path string true "id" // @Param id path int true "id"
// @Tags Source // @Tags Source
// @Router /sources/{id} [POST] // @Router /v1/sources/{id} [POST]
// @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) deleteSources(c echo.Context) error { func (s *Handler) deleteSources(c echo.Context) error {
id := c.Param("ID") p := domain.SourcesResponse{
uuid, err := uuid.Parse(id) BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
}
_, err := s.ValidateJwtToken(c, domain.ScopeAll)
if err != nil { if err != nil {
return c.JSON(http.StatusBadRequest, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusUnauthorized, p)
}) }
id, err := strconv.Atoi(c.Param("ID"))
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
// Check to make sure we can find the record // Check to make sure we can find the record
_, err = s.Db.GetSourceByID(c.Request().Context(), uuid) _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusInternalServerError, p)
})
} }
// Delete the record // Delete the record
err = s.Db.DeleteSource(c.Request().Context(), uuid) rows, err := s.repo.Sources.SoftDelete(c.Request().Context(), int64(id))
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusInternalServerError, p)
}) }
if rows != 1 {
p.BaseResponse.Message = ErrFailedToUpdateRecord
return c.JSON(http.StatusInternalServerError, p)
} }
p := ApiStatusModel{ // pull the record with its updated value
Message: "OK", item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id))
StatusCode: http.StatusOK,
}
b, err := json.Marshal(p)
if err != nil { if err != nil {
return c.JSON(http.StatusInternalServerError, domain.BaseResponse{ p.BaseResponse.Message = err.Error()
Message: err.Error(), return c.JSON(http.StatusInternalServerError, p)
})
} }
return c.JSON(http.StatusOK, b) var items []domain.SourceDto
items = append(items, dtoconv.SourceToDto(item))
p.Payload = items
p.IsError = false
return c.JSON(http.StatusOK, p)
} }
// DisableSource // DisableSource
// @Summary Disables a source from processing. // @Summary Disables a source from processing.
// @Param id path int true "id" // @Param id path int true "id"
// @Tags Source // @Tags Source
// @Router /sources/{id}/disable [post] // @Router /v1/sources/{id}/disable [post]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) disableSource(c echo.Context) error { func (s *Handler) disableSource(c echo.Context) error {
resp := domain.SourcesResponse { p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeAll)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
// Check to make sure we can find the record // Check to make sure we can find the record
_, err = s.repo.Sources.GetById(c.Request().Context(), int64(id)) _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
_, err = s.repo.Sources.Disable(c.Request().Context(), int64(id)) _, err = s.repo.Sources.Disable(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dto []domain.SourceDto var dto []domain.SourceDto
dto = append(dto, services.SourceToDto(item)) dto = append(dto, dtoconv.SourceToDto(item))
resp.Payload = dto p.Payload = dto
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }
// EnableSource // EnableSource
// @Summary Enables a source to continue processing. // @Summary Enables a source to continue processing.
// @Param id path string true "id" // @Param id path string true "id"
// @Tags Source // @Tags Source
// @Router /sources/{id}/enable [post] // @Router /v1/sources/{id}/enable [post]
// @Success 200 {object} domain.SourcesResponse "ok" // @Success 200 {object} domain.SourcesResponse "ok"
// @Failure 400 {object} domain.BaseResponse // @Failure 400 {object} domain.SourcesResponse
// @Failure 500 {object} domain.BaseResponse // @Failure 500 {object} domain.SourcesResponse
// @Security Bearer
func (s *Handler) enableSource(c echo.Context) error { func (s *Handler) enableSource(c echo.Context) error {
resp := domain.SourcesResponse { p := domain.SourcesResponse{
BaseResponse: domain.BaseResponse{ BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess, Message: ResponseMessageSuccess,
IsError: true,
}, },
} }
_, err := s.ValidateJwtToken(c, domain.ScopeAll)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
id, err := strconv.Atoi(c.Param("ID")) id, err := strconv.Atoi(c.Param("ID"))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
// Check to make sure we can find the record // Check to make sure we can find the record
_, err = s.repo.Sources.GetById(c.Request().Context(), int64(id)) _, err = s.repo.Sources.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusBadRequest) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
} }
_, err = s.repo.Sources.Enable(c.Request().Context(), int64(id)) _, err = s.repo.Sources.Enable(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id)) item, err := s.repo.Sources.GetById(c.Request().Context(), int64(id))
if err != nil { if err != nil {
s.WriteError(c, err, http.StatusInternalServerError) p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
} }
var dto []domain.SourceDto var dto []domain.SourceDto
dto = append(dto, services.SourceToDto(item)) dto = append(dto, dtoconv.SourceToDto(item))
resp.Payload = dto p.Payload = dto
return c.JSON(http.StatusOK, resp) p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
} }

View File

@ -1,230 +0,0 @@
package v1
import (
"context"
"encoding/json"
"errors"
"net/http"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
type ListSubscriptions struct {
ApiStatusModel
Payload []models.SubscriptionDto `json:"payload"`
}
type GetSubscription struct {
ApiStatusModel
Payload models.SubscriptionDto `json:"payload"`
}
type ListSubscriptionDetails struct {
ApiStatusModel
Payload []models.SubscriptionDetailsDto `json:"payload"`
}
// GetSubscriptions
// @Summary Returns the top 100 entries from the queue to be processed.
// @Produce application/json
// @Tags Subscription
// @Router /subscriptions [get]
// @Success 200 {object} ListSubscriptions "ok"
// @Failure 400 {object} ApiError "Unable to reach SQL."
// @Failure 500 {object} ApiError "Failed to process data from SQL."
func (s *Handler) ListSubscriptions(c echo.Context) error {
payload := ListSubscriptions{
ApiStatusModel: ApiStatusModel{
StatusCode: http.StatusOK,
Message: "OK",
},
}
res, err := s.dto.ListSubscriptions(c.Request().Context(), 50)
if err != nil {
return s.WriteError(c, err, http.StatusBadRequest)
}
payload.Payload = res
return c.JSON(http.StatusOK, payload)
}
// ListSubscriptionDetails
// @Summary Returns the top 50 entries with full deatils on the source and output.
// @Produce application/json
// @Tags Subscription
// @Router /subscriptions/details [get]
// @Success 200 {object} ListSubscriptionDetails "ok"
func (s *Handler) ListSubscriptionDetails(c echo.Context) error {
payload := ListSubscriptionDetails{
ApiStatusModel: ApiStatusModel{
StatusCode: http.StatusOK,
Message: "OK",
},
}
res, err := s.dto.ListSubscriptionDetails(c.Request().Context(), 50)
if err != nil {
return s.WriteError(c, err, http.StatusInternalServerError)
}
payload.Payload = res
return c.JSON(http.StatusOK, payload)
}
// GetSubscriptionsByDiscordId
// @Summary Returns the top 100 entries from the queue to be processed.
// @Produce application/json
// @Param id query string true "id"
// @Tags Subscription
// @Router /subscriptions/by/discordId [get]
// @Success 200 {object} ListSubscriptions "ok"
// @Failure 400 {object} ApiError "Unable to reach SQL or Data problems"
// @Failure 500 {object} ApiError "Data problems"
func (s *Handler) GetSubscriptionsByDiscordId(c echo.Context) error {
p := ListSubscriptions{
ApiStatusModel: ApiStatusModel{
StatusCode: http.StatusOK,
Message: "OK",
},
}
id := c.QueryParam("id")
if id == "" {
return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest)
}
uuid, err := uuid.Parse(id)
if err != nil {
return s.WriteError(c, errors.New(ErrValueNotUuid), http.StatusBadRequest)
}
res, err := s.dto.ListSubscriptionsByDiscordWebhookId(context.Background(), uuid)
if err != nil {
return s.WriteError(c, err, http.StatusNoContent)
}
p.Payload = res
return c.JSON(http.StatusOK, p)
}
// GetSubscriptionsBySourceId
// @Summary Returns the top 100 entries from the queue to be processed.
// @Produce application/json
// @Param id query string true "id"
// @Tags Subscription
// @Router /subscriptions/by/SourceId [get]
// @Success 200 {object} ListSubscriptions "ok"
func (s *Handler) GetSubscriptionsBySourceId(c echo.Context) error {
p := ListSubscriptions{
ApiStatusModel: ApiStatusModel{
StatusCode: http.StatusOK,
Message: "OK",
},
}
_id := c.QueryParam("id")
if _id == "" {
return s.WriteError(c, errors.New(ErrIdValueMissing), http.StatusBadRequest)
}
uuid, err := uuid.Parse(_id)
if err != nil {
return s.WriteError(c, err, http.StatusBadRequest)
}
res, err := s.dto.ListSubscriptionsBySourceId(context.Background(), uuid)
if err != nil {
return s.WriteError(c, err, http.StatusNoContent)
}
p.Payload = res
return c.JSON(http.StatusOK, p)
}
// NewDiscordWebHookSubscription
// @Summary Creates a new subscription to link a post from a Source to a DiscordWebHook.
// @Param discordWebHookId query string true "discordWebHookId"
// @Param sourceId query string true "sourceId"
// @Tags Subscription
// @Router /subscriptions/discord/webhook/new [post]
func (s *Handler) newDiscordWebHookSubscription(c echo.Context) error {
// Extract the values given
discordWebHookId := c.QueryParam("discordWebHookId")
sourceId := c.QueryParam("sourceId")
// Check to make we didn't get a null
if discordWebHookId == "" {
return s.WriteError(c, errors.New("invalid discordWebHooksId given"), http.StatusBadRequest)
}
if sourceId == "" {
return s.WriteError(c, errors.New("invalid sourceID given"), http.StatusBadRequest)
}
// Validate they are UUID values
uHook, err := uuid.Parse(discordWebHookId)
if err != nil {
return s.WriteError(c, err, http.StatusBadRequest)
}
uSource, err := uuid.Parse(sourceId)
if err != nil {
return s.WriteError(c, err, http.StatusBadRequest)
}
// Check if the sub already exists
_, err = s.Db.QuerySubscriptions(c.Request().Context(), database.QuerySubscriptionsParams{
Discordwebhookid: uHook,
Sourceid: uSource,
})
if err == nil {
return s.WriteError(c, errors.New("a subscription already exists between these two entities"), http.StatusBadRequest)
}
// Does not exist, so make it.
params := database.CreateSubscriptionParams{
ID: uuid.New(),
Discordwebhookid: uHook,
Sourceid: uSource,
}
err = s.Db.CreateSubscription(context.Background(), params)
if err != nil {
return s.WriteError(c, err, http.StatusInternalServerError)
}
bJson, err := json.Marshal(&params)
if err != nil {
return s.WriteError(c, err, http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, bJson)
}
// DeleteDiscordWebHookSubscription
// @Summary Removes a Discord WebHook Subscription based on the Subscription ID.
// @Param id query string true "id"
// @Tags Subscription
// @Router /subscriptions/discord/webhook/delete [delete]
func (s *Handler) DeleteDiscordWebHookSubscription(c echo.Context) error {
var ErrMissingSubscriptionID string = "the request was missing a 'Id'"
id := c.QueryParam("id")
if id == "" {
return s.WriteError(c, errors.New(ErrMissingSubscriptionID), http.StatusBadRequest)
}
uid, err := uuid.Parse(id)
if err != nil {
return s.WriteError(c, err, http.StatusBadRequest)
}
err = s.Db.DeleteSubscription(context.Background(), uid)
if err != nil {
return s.WriteError(c, err, http.StatusInternalServerError)
}
return c.JSON(http.StatusOK, nil)
}

View File

@ -0,0 +1,351 @@
package v1
import (
"net/http"
"strings"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
)
const (
ErrUserNotFound = "requested user does not exist"
ErrUsernameAlreadyExists = "the requested username already exists"
)
// @Summary Creates a new user
// @Router /v1/users/register [post]
// @Param request formData domain.LoginFormRequest true "form"
// @Accepts x-www-form-urlencoded
// @Produce json
// @Tags Users
// @Success 201 {object} domain.BaseResponse
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
func (h *Handler) AuthRegister(c echo.Context) error {
p := domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
}
username := c.FormValue("username")
password := c.FormValue("password")
//username := c.QueryParam("username")
exists, err := h.repo.Users.GetUser(c.Request().Context(), username)
if err != nil {
// if we have an err, validate that if its not user not found.
// if the user is not found, we can use that name
if err.Error() != repository.ErrUserNotFound {
p.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
}
if exists.Username == username {
p.Message = ErrUsernameAlreadyExists
return c.JSON(http.StatusInternalServerError, p)
}
//password := c.QueryParam("password")
err = h.repo.Users.CheckPasswordForRequirements(password)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
_, err = h.repo.Users.Create(c.Request().Context(), username, password, domain.ScopeArticleRead)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
return c.JSON(http.StatusCreated, p)
}
// @Summary Logs into the API and returns a bearer token if successful
// @Router /v1/users/login [post]
// @Param request formData domain.LoginFormRequest true "form"
// @Accepts x-www-form-urlencoded
// @Produce json
// @Tags Users
// @Success 200 {object} domain.LoginResponse
// @Failure 400 {object} domain.LoginResponse
// @Failure 500 {object} domain.LoginResponse
func (h *Handler) AuthLogin(c echo.Context) error {
p := domain.LoginResponse{
BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
//Token: jwt,
Type: "Bearer",
//RefreshToken: refresh,
}
username := c.FormValue("username")
password := c.FormValue("password")
// Check to see if they are trying to login with the admin token
if username == "" {
return h.createAdminToken(c, password)
}
// check if the user exists
user, err := h.repo.Users.GetUser(c.Request().Context(), username)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
// make sure the hash matches
err = h.repo.Users.DoesPasswordMatchHash(c.Request().Context(), username, password)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
// TODO think about moving this down some?
expiresAt := time.Now().Add(time.Hour * 48)
userScopes := strings.Split(user.Scopes, ",")
jwt, err := h.generateJwtWithExp(username, h.config.ServerAddress, user.SessionToken, userScopes, user.ID, expiresAt)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
refresh, err := h.repo.RefreshTokens.Create(c.Request().Context(), username)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
p.Token = jwt
p.RefreshToken = refresh
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
}
func (h *Handler) createAdminToken(c echo.Context, password string) error {
p := domain.LoginResponse{
BaseResponse: domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
},
//Token: token,
Type: "Bearer",
}
// if the admin token is blank, then the admin wanted this disabled.
// this will fail right away and not progress.
if h.config.AdminSecret == "" {
p.BaseResponse.Message = ErrUserNotFound
return c.JSON(http.StatusBadRequest, p)
}
if h.config.AdminSecret != password {
p.BaseResponse.Message = ErrUserNotFound
return c.JSON(http.StatusBadRequest, p)
}
var userScopes []string
userScopes = append(userScopes, domain.ScopeAll)
sessionToken, err := uuid.NewV7()
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
token, err := h.generateJwt("admin", h.config.ServerAddress, sessionToken.String(), userScopes, -1)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
p.Token = token
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
}
// This will take collect some information about the requested refresh, validate and then return a new jwt token if approved.
// Register
// @Summary Generates a new token
// @Router /v1/users/refresh/token [post]
// @Param request body domain.RefreshTokenRequest true "body"
// @Tags Users
// @Success 200 {object} domain.LoginResponse
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (h *Handler) RefreshJwtToken(c echo.Context) error {
p := domain.LoginResponse{
BaseResponse: domain.BaseResponse{
Message: "OK",
},
//Token: jwt,
Type: "Bearer",
//RefreshToken: newRefreshToken,
}
_, err := h.ValidateJwtToken(c, domain.ScopeDiscordWebHookCreate)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
// Check the context for the refresh token
var request domain.RefreshTokenRequest
err = (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
err = h.repo.RefreshTokens.IsRequestValid(c.Request().Context(), request.Username, request.RefreshToken)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
user, err := h.repo.Users.GetUser(c.Request().Context(), request.Username)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
userScopes := strings.Split(user.Scopes, ",")
jwt, err := h.generateJwtWithExp(request.Username, h.config.ServerAddress, user.SessionToken, userScopes, user.ID, time.Now().Add(time.Hour*48))
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
newRefreshToken, err := h.repo.RefreshTokens.Create(c.Request().Context(), request.Username)
if err != nil {
p.BaseResponse.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
p.Token = jwt
p.RefreshToken = newRefreshToken
p.BaseResponse.IsError = false
return c.JSON(http.StatusOK, p)
}
// @Summary Adds a new scope to a user account
// @Router /v1/users/scopes/add [post]
// @Param request body domain.UpdateScopesRequest true "body"
// @Tags Users
// @Accept json
// @Produce json
// @Success 200 {object} domain.BaseResponse
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (h *Handler) AddScopes(c echo.Context) error {
p := domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
}
_, err := h.ValidateJwtToken(c, domain.ScopeAll)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
request := domain.UpdateScopesRequest{}
err = (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
err = h.repo.Users.AddScopes(c.Request().Context(), request.Username, request.Scopes)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
p.IsError = false
return c.JSON(http.StatusOK, p)
}
// @Summary Adds a new scope to a user account
// @Router /v1/users/scopes/remove [post]
// @Param request body domain.UpdateScopesRequest true "body"
// @Tags Users
// @Accept json
// @Produce json
// @Success 200 {object} domain.BaseResponse
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (h *Handler) RemoveScopes(c echo.Context) error {
p := domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
}
token, err := h.getJwtTokenFromContext(c)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
err = token.IsValid(domain.ScopeAll)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
request := domain.UpdateScopesRequest{}
err = (&echo.DefaultBinder{}).BindBody(c, &request)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusBadRequest, p)
}
err = h.repo.Users.RemoveScopes(c.Request().Context(), request.Username, request.Scopes)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
p.IsError = false
return c.JSON(http.StatusOK, p)
}
// @Summary Revokes the current session token and replaces it with a new one.
// @Router /v1/users/refresh/sessionToken [post]
// @Tags Users
// @Accept json
// @Produce json
// @Success 200 {object} domain.BaseResponse
// @Failure 400 {object} domain.BaseResponse
// @Failure 500 {object} domain.BaseResponse
// @Security Bearer
func (h *Handler) NewSessionToken(c echo.Context) error {
p := domain.BaseResponse{
Message: ResponseMessageSuccess,
IsError: true,
}
token, err := h.getJwtTokenFromContext(c)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusUnauthorized, p)
}
_, err = h.repo.Users.NewSessionToken(c.Request().Context(), token.UserName)
if err != nil {
p.Message = err.Error()
return c.JSON(http.StatusInternalServerError, p)
}
p.IsError = false
return c.JSON(http.StatusInternalServerError, p)
}

View File

@ -0,0 +1,122 @@
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder"
)
type AlertDiscordRepo interface {
Create(ctx context.Context, userId, sourceId, webhookId int64) (int64, error)
SoftDelete(ctx context.Context, id int64) (int64, error)
Restore(ctx context.Context, id int64) (int64, error)
Delete(ctx context.Context, id int64) (int64, error)
ListByUser(ctx context.Context, page, limit int, userId int64) ([]entity.AlertDiscordEntity, error)
}
type alertDiscordRepository struct {
conn *sql.DB
defaultLimit int
defaultOffset int
}
func NewAlertDiscordRepository(conn *sql.DB) alertDiscordRepository {
return alertDiscordRepository{
conn: conn,
defaultLimit: 50,
defaultOffset: 50,
}
}
func (r alertDiscordRepository) Create(ctx context.Context, userId, sourceId, webhookId int64) (int64, error) {
dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("AlertDiscord")
queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "SourceID", "DiscordWebHookID")
queryBuilder.Values(dt, dt, timeZero, userId, sourceId, webhookId)
query, args := queryBuilder.Build()
_, err := r.conn.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
return 1, nil
}
func (r alertDiscordRepository) SoftDelete(ctx context.Context, id int64) (int64, error) {
return softDeleteRow(ctx, r.conn, "AlertDiscord", id)
}
func (r alertDiscordRepository) Restore(ctx context.Context, id int64) (int64, error) {
return restoreRow(ctx, r.conn, "AlertDiscord", id)
}
func (r alertDiscordRepository) Delete(ctx context.Context, id int64) (int64, error) {
return deleteFromTable(ctx, r.conn, "AlertDiscord", id)
}
func (r alertDiscordRepository) ListByUser(ctx context.Context, page, limit int, userId int64) ([]entity.AlertDiscordEntity, error) {
builder := sqlbuilder.NewSelectBuilder()
builder.Select("*")
builder.From("AlertDiscord")
builder.Where(
builder.Equal("UserID", userId),
)
builder.Offset(page * limit)
builder.Limit(limit)
query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil {
return []entity.AlertDiscordEntity{}, err
}
data := r.processRows(rows)
if len(data) == 0 {
return []entity.AlertDiscordEntity{}, errors.New(ErrUserNotFound)
}
return data, nil
}
func (ur alertDiscordRepository) processRows(rows *sql.Rows) []entity.AlertDiscordEntity {
items := []entity.AlertDiscordEntity{}
for rows.Next() {
var id int64
var createdAt time.Time
var updatedAt time.Time
var deletedAt time.Time
var userId int64
var sourceId int64
var webhookId int64
err := rows.Scan(
&id, &createdAt, &updatedAt, &deletedAt,
&userId, &sourceId, &webhookId,
)
if err != nil {
fmt.Println(err)
}
item := entity.AlertDiscordEntity{
ID: id,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
UserID: userId,
SourceID: sourceId,
DiscordWebHookId: webhookId,
}
items = append(items, item)
}
return items
}

View File

@ -0,0 +1,63 @@
package repository_test
import (
"context"
"testing"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
)
func TestAlertDiscordCreate(t *testing.T) {
t.Log(time.Time{})
db, err := setupInMemoryDb()
if err != nil {
t.Log(err)
t.FailNow()
}
defer db.Close()
r := repository.NewAlertDiscordRepository(db)
created, err := r.Create(context.Background(), 1, 1, 1)
if err != nil {
t.Log(err)
t.FailNow()
}
if created != 1 {
t.Log("failed to create the record")
t.FailNow()
}
}
func TestAlertDiscordCreateAndValidate(t *testing.T) {
t.Log(time.Time{})
db, err := setupInMemoryDb()
if err != nil {
t.Log(err)
t.FailNow()
}
defer db.Close()
source := repository.NewSourceRepository(db)
source.Create(context.Background(), domain.SourceCollectorRss, "Unit Testing", "www.fake.com", "testing,units", true)
sourceRecord, _ := source.GetBySourceAndName(context.Background(), domain.SourceCollectorRss, "Unit Testing")
webhookRepo := repository.NewDiscordWebHookRepository(db)
webhookRepo.Create(context.Background(), 999, "discord.com", "Unit Testing", "memes", true)
webhook, _ := webhookRepo.GetByUrl(context.Background(), "discord.com")
r := repository.NewAlertDiscordRepository(db)
r.Create(context.Background(), 999, sourceRecord.ID, webhook.ID)
alert, err := r.ListByUser(context.Background(), 0, 10, 999)
if err != nil {
t.Error(err)
t.FailNow()
}
if len(alert) != 1 {
t.Error("got the incorrect number of rows back")
t.FailNow()
}
}

View File

@ -7,7 +7,7 @@ import (
"fmt" "fmt"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
) )
@ -17,13 +17,14 @@ const (
) )
type ArticlesRepo interface { type ArticlesRepo interface {
GetById(ctx context.Context, id int64) (domain.ArticleEntity, error) GetById(ctx context.Context, id int64) (entity.ArticleEntity, error)
GetByUrl(ctx context.Context, url string) (domain.ArticleEntity, error) GetByUrl(ctx context.Context, url string) (entity.ArticleEntity, error)
ListTop(ctx context.Context, limit int) ([]domain.ArticleEntity, error) ListTop(ctx context.Context, limit int) ([]entity.ArticleEntity, error)
ListByPage(ctx context.Context, page, limit int) ([]domain.ArticleEntity, error) ListByPage(ctx context.Context, page, limit int) ([]entity.ArticleEntity, error)
ListByPublishDate(ctx context.Context, page, limit int, orderBy string) ([]domain.ArticleEntity, error) ListByPublishDate(ctx context.Context, page, limit int, orderBy string) ([]entity.ArticleEntity, error)
ListBySource(ctx context.Context, page, limit, sourceId int, orderBy string) ([]domain.ArticleEntity, error) ListBySource(ctx context.Context, page, limit, sourceId int, orderBy string) ([]entity.ArticleEntity, error)
Create(ctx context.Context, sourceId int64, tags, title, url, thumbnailUrl, description, authorName, authorImageUrl string, pubDate time.Time, isVideo bool) (int64, error) Create(ctx context.Context, sourceId int64, tags, title, url, thumbnailUrl, description, authorName, authorImageUrl string, pubDate time.Time, isVideo bool) (int64, error)
CreateFromEntity(ctx context.Context, entity entity.ArticleEntity) (int64, error)
} }
type ArticleRepository struct { type ArticleRepository struct {
@ -40,7 +41,7 @@ func NewArticleRepository(conn *sql.DB) ArticleRepository {
} }
} }
func (ar ArticleRepository) GetById(ctx context.Context, id int64) (domain.ArticleEntity, error) { func (ar ArticleRepository) GetById(ctx context.Context, id int64) (entity.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("articles").Where( builder.From("articles").Where(
@ -51,18 +52,18 @@ func (ar ArticleRepository) GetById(ctx context.Context, id int64) (domain.Artic
query, args := builder.Build() query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, query, args...) rows, err := ar.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.ArticleEntity{}, err return entity.ArticleEntity{}, err
} }
data := ar.processRows(rows) data := ar.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.ArticleEntity{}, errors.New(ErrUserNotFound) return entity.ArticleEntity{}, errors.New(ErrUserNotFound)
} }
return data[0], nil return data[0], nil
} }
func (ar ArticleRepository) GetByUrl(ctx context.Context, url string) (domain.ArticleEntity, error) { func (ar ArticleRepository) GetByUrl(ctx context.Context, url string) (entity.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("articles").Where( builder.From("articles").Where(
@ -73,18 +74,18 @@ func (ar ArticleRepository) GetByUrl(ctx context.Context, url string) (domain.Ar
query, args := builder.Build() query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, query, args...) rows, err := ar.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.ArticleEntity{}, err return entity.ArticleEntity{}, err
} }
data := ar.processRows(rows) data := ar.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.ArticleEntity{}, errors.New(ErrUserNotFound) return entity.ArticleEntity{}, errors.New(ErrUserNotFound)
} }
return data[0], nil return data[0], nil
} }
func (ar ArticleRepository) ListTop(ctx context.Context, limit int) ([]domain.ArticleEntity, error) { func (ar ArticleRepository) ListTop(ctx context.Context, limit int) ([]entity.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("articles") builder.From("articles")
@ -93,18 +94,18 @@ func (ar ArticleRepository) ListTop(ctx context.Context, limit int) ([]domain.Ar
query, args := builder.Build() query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, query, args...) rows, err := ar.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.ArticleEntity{}, err return []entity.ArticleEntity{}, err
} }
data := ar.processRows(rows) data := ar.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) return []entity.ArticleEntity{}, errors.New(ErrUserNotFound)
} }
return data, nil return data, nil
} }
func (ar ArticleRepository) ListByPage(ctx context.Context, page, limit int) ([]domain.ArticleEntity, error) { func (ar ArticleRepository) ListByPage(ctx context.Context, page, limit int) ([]entity.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("articles") builder.From("articles")
@ -115,18 +116,18 @@ func (ar ArticleRepository) ListByPage(ctx context.Context, page, limit int) ([]
query, args := builder.Build() query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, query, args...) rows, err := ar.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.ArticleEntity{}, err return []entity.ArticleEntity{}, err
} }
data := ar.processRows(rows) data := ar.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) return []entity.ArticleEntity{}, errors.New(ErrUserNotFound)
} }
return data, nil return data, nil
} }
func (ar ArticleRepository) ListByPublishDate(ctx context.Context, page, limit int, orderBy string) ([]domain.ArticleEntity, error) { func (ar ArticleRepository) ListByPublishDate(ctx context.Context, page, limit int, orderBy string) ([]entity.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("articles") builder.From("articles")
@ -139,17 +140,17 @@ func (ar ArticleRepository) ListByPublishDate(ctx context.Context, page, limit i
query, args := builder.Build() query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, query, args...) rows, err := ar.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.ArticleEntity{}, err return []entity.ArticleEntity{}, err
} }
data := ar.processRows(rows) data := ar.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) return []entity.ArticleEntity{}, errors.New(ErrUserNotFound)
} }
return data, nil return data, nil
} }
func (ar ArticleRepository) ListBySource(ctx context.Context, page, limit, sourceId int, orderBy string) ([]domain.ArticleEntity, error) { func (ar ArticleRepository) ListBySource(ctx context.Context, page, limit, sourceId int, orderBy string) ([]entity.ArticleEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("articles") builder.From("articles")
@ -166,12 +167,12 @@ func (ar ArticleRepository) ListBySource(ctx context.Context, page, limit, sourc
query, args := builder.Build() query, args := builder.Build()
rows, err := ar.conn.QueryContext(ctx, query, args...) rows, err := ar.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.ArticleEntity{}, err return []entity.ArticleEntity{}, err
} }
data := ar.processRows(rows) data := ar.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.ArticleEntity{}, errors.New(ErrUserNotFound) return []entity.ArticleEntity{}, errors.New(ErrUserNotFound)
} }
return data, nil return data, nil
} }
@ -192,8 +193,24 @@ func (ar ArticleRepository) Create(ctx context.Context, sourceId int64, tags, ti
return 1, nil return 1, nil
} }
func (ur ArticleRepository) processRows(rows *sql.Rows) []domain.ArticleEntity { func (ar ArticleRepository) CreateFromEntity(ctx context.Context, entity entity.ArticleEntity) (int64, error) {
items := []domain.ArticleEntity{} dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("articles")
queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "SourceId", "Tags", "Title", "Url", "PubDate", "IsVideo", "ThumbnailUrl", "Description", "AuthorName", "AuthorImageUrl")
queryBuilder.Values(dt, dt, timeZero, entity.SourceID, entity.Tags, entity.Title, entity.Url, entity.PubDate, entity.IsVideo, entity.Thumbnail, entity.Description, entity.AuthorName, entity.AuthorImageUrl)
query, args := queryBuilder.Build()
_, err := ar.conn.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
return 1, nil
}
func (ur ArticleRepository) processRows(rows *sql.Rows) []entity.ArticleEntity {
items := []entity.ArticleEntity{}
for rows.Next() { for rows.Next() {
var id int64 var id int64
@ -220,7 +237,7 @@ func (ur ArticleRepository) processRows(rows *sql.Rows) []domain.ArticleEntity {
fmt.Println(err) fmt.Println(err)
} }
item := domain.ArticleEntity{ item := entity.ArticleEntity{
ID: id, ID: id,
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,

View File

@ -5,21 +5,21 @@ import (
"database/sql" "database/sql"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
) )
type DiscordWebHookRepo interface{ type DiscordWebHookRepo interface {
Create(ctx context.Context, url, server, channel string, enabled bool) (int64, error) Create(ctx context.Context, userId int64, url, server, channel string, enabled bool) (int64, error)
Enable(ctx context.Context, id int64) (int64, error) Enable(ctx context.Context, id int64) (int64, error)
Disable(ctx context.Context, id int64) (int64, error) Disable(ctx context.Context, id int64) (int64, error)
SoftDelete(ctx context.Context, id int64) (int64, error) SoftDelete(ctx context.Context, id int64) (int64, error)
Restore(ctx context.Context, id int64) (int64, error) Restore(ctx context.Context, id int64) (int64, error)
Delete(ctx context.Context, id int64) (int64, error) Delete(ctx context.Context, id int64) (int64, error)
GetById(ctx context.Context, id int64) (domain.DiscordWebHookEntity, error) GetById(ctx context.Context, id int64) (entity.DiscordWebHookEntity, error)
GetByUrl(ctx context.Context, url string) (domain.DiscordWebHookEntity, error) GetByUrl(ctx context.Context, url string) (entity.DiscordWebHookEntity, error)
ListByServerName(ctx context.Context, name string) ([]domain.DiscordWebHookEntity, error) ListByServerName(ctx context.Context, name string) ([]entity.DiscordWebHookEntity, error)
ListByServerAndChannel(ctx context.Context, server, channel string) ([]domain.DiscordWebHookEntity, error) ListByServerAndChannel(ctx context.Context, server, channel string) ([]entity.DiscordWebHookEntity, error)
} }
type discordWebHookRepository struct { type discordWebHookRepository struct {
@ -32,12 +32,12 @@ func NewDiscordWebHookRepository(conn *sql.DB) discordWebHookRepository {
} }
} }
func (r discordWebHookRepository) Create(ctx context.Context, url, server, channel string, enabled bool) (int64, error) { func (r discordWebHookRepository) Create(ctx context.Context, userId int64, url, server, channel string, enabled bool) (int64, error) {
dt := time.Now() dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("DiscordWebHooks") queryBuilder.InsertInto("DiscordWebHooks")
queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "Url", "Server", "Channel", "Enabled") queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "Url", "Server", "Channel", "Enabled")
queryBuilder.Values(dt, dt, timeZero, url, server, channel, enabled) queryBuilder.Values(dt, dt, timeZero, userId, url, server, channel, enabled)
query, args := queryBuilder.Build() query, args := queryBuilder.Build()
_, err := r.conn.ExecContext(ctx, query, args...) _, err := r.conn.ExecContext(ctx, query, args...)
@ -100,7 +100,7 @@ func (r discordWebHookRepository) Delete(ctx context.Context, id int64) (int64,
return deleteFromTable(ctx, r.conn, "DiscordWebHooks", id) return deleteFromTable(ctx, r.conn, "DiscordWebHooks", id)
} }
func (r discordWebHookRepository) GetById(ctx context.Context, id int64) (domain.DiscordWebHookEntity, error) { func (r discordWebHookRepository) GetById(ctx context.Context, id int64) (entity.DiscordWebHookEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("DiscordWebHooks").Where( builder.From("DiscordWebHooks").Where(
@ -111,18 +111,18 @@ func (r discordWebHookRepository) GetById(ctx context.Context, id int64) (domain
query, args := builder.Build() query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.DiscordWebHookEntity{}, err return entity.DiscordWebHookEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.DiscordWebHookEntity{}, err return entity.DiscordWebHookEntity{}, err
} }
return data[0], nil return data[0], nil
} }
func (r discordWebHookRepository) GetByUrl(ctx context.Context, url string) (domain.DiscordWebHookEntity, error) { func (r discordWebHookRepository) GetByUrl(ctx context.Context, url string) (entity.DiscordWebHookEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("DiscordWebHooks").Where( builder.From("DiscordWebHooks").Where(
@ -133,18 +133,18 @@ func (r discordWebHookRepository) GetByUrl(ctx context.Context, url string) (dom
query, args := builder.Build() query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.DiscordWebHookEntity{}, err return entity.DiscordWebHookEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.DiscordWebHookEntity{}, err return entity.DiscordWebHookEntity{}, err
} }
return data[0], nil return data[0], nil
} }
func (r discordWebHookRepository) ListByServerName(ctx context.Context, name string) ([]domain.DiscordWebHookEntity, error) { func (r discordWebHookRepository) ListByServerName(ctx context.Context, name string) ([]entity.DiscordWebHookEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("DiscordWebHooks").Where( builder.From("DiscordWebHooks").Where(
@ -154,18 +154,18 @@ func (r discordWebHookRepository) ListByServerName(ctx context.Context, name str
query, args := builder.Build() query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.DiscordWebHookEntity{}, err return []entity.DiscordWebHookEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.DiscordWebHookEntity{}, err return []entity.DiscordWebHookEntity{}, err
} }
return data, nil return data, nil
} }
func (r discordWebHookRepository) ListByServerAndChannel(ctx context.Context, server, channel string) ([]domain.DiscordWebHookEntity, error) { func (r discordWebHookRepository) ListByServerAndChannel(ctx context.Context, server, channel string) ([]entity.DiscordWebHookEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("DiscordWebHooks").Where( builder.From("DiscordWebHooks").Where(
@ -176,43 +176,45 @@ func (r discordWebHookRepository) ListByServerAndChannel(ctx context.Context, se
query, args := builder.Build() query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.DiscordWebHookEntity{}, err return []entity.DiscordWebHookEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.DiscordWebHookEntity{}, err return []entity.DiscordWebHookEntity{}, err
} }
return data, nil return data, nil
} }
func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]domain.DiscordWebHookEntity, error) { func (r discordWebHookRepository) processRows(rows *sql.Rows) ([]entity.DiscordWebHookEntity, error) {
items := []domain.DiscordWebHookEntity{} items := []entity.DiscordWebHookEntity{}
for rows.Next() { for rows.Next() {
var id int64 var id int64
var createdAt time.Time var createdAt time.Time
var updatedAt time.Time var updatedAt time.Time
var deletedAt time.Time var deletedAt time.Time
var userId int64
var url string var url string
var server string var server string
var channel string var channel string
var enabled bool var enabled bool
err := rows.Scan( err := rows.Scan(
&id, &createdAt, &updatedAt, &id, &createdAt, &updatedAt,
&deletedAt, &url, &server, &deletedAt, &userId, &url, &server,
&channel, &enabled, &channel, &enabled,
) )
if err != nil { if err != nil {
return items, err return items, err
} }
item := domain.DiscordWebHookEntity{ item := entity.DiscordWebHookEntity{
ID: id, ID: id,
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
DeletedAt: deletedAt, DeletedAt: deletedAt,
UserID: userId,
Url: url, Url: url,
Server: server, Server: server,
Channel: channel, Channel: channel,

View File

@ -17,7 +17,7 @@ func TestCreateDiscordWebHookRecord(t *testing.T) {
defer db.Close() defer db.Close()
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
created, err := r.Create(context.Background(), "www.discord.com/bad/webhook", "Unit Testing", "memes", true) created, err := r.Create(context.Background(), 999, "www.discord.com/bad/webhook", "Unit Testing", "memes", true)
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -38,7 +38,7 @@ func TestDiscordWebHookGetById(t *testing.T) {
defer db.Close() defer db.Close()
ctx := context.Background() ctx := context.Background()
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
created, err := r.Create(ctx, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) created, err := r.Create(ctx, 999, "www.discord.com/bad/webhook", "Unit Testing", "memes", true)
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -71,7 +71,7 @@ func TestDiscordWebHookGetByUrl(t *testing.T) {
ctx := context.Background() ctx := context.Background()
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", "Unit Testing", "memes", true) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", "Unit Testing", "memes", true)
item, err := r.GetByUrl(ctx, "www.discord.com/bad/webhook") item, err := r.GetByUrl(ctx, "www.discord.com/bad/webhook")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
@ -95,7 +95,7 @@ func TestDiscordWebHookListByServerName(t *testing.T) {
ctx := context.Background() ctx := context.Background()
serverName := "Unit Testing" serverName := "Unit Testing"
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, "memes", true) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, "memes", true)
item, err := r.ListByServerName(ctx, serverName) item, err := r.ListByServerName(ctx, serverName)
if err != nil { if err != nil {
@ -121,7 +121,7 @@ func TestDiscordWebHookListByServerAndChannel(t *testing.T) {
serverName := "Unit Testing" serverName := "Unit Testing"
channel := "memes" channel := "memes"
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true)
item, err := r.ListByServerAndChannel(ctx, serverName, channel) item, err := r.ListByServerAndChannel(ctx, serverName, channel)
if err != nil { if err != nil {
@ -152,7 +152,7 @@ func TestDiscordWebHookEnableRecord(t *testing.T) {
serverName := "Unit Testing" serverName := "Unit Testing"
channel := "memes" channel := "memes"
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, false) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, false)
item, err := r.GetById(ctx, 1) item, err := r.GetById(ctx, 1)
if err != nil { if err != nil {
@ -195,7 +195,7 @@ func TestDiscordWebHookDisableRecord(t *testing.T) {
serverName := "Unit Testing" serverName := "Unit Testing"
channel := "memes" channel := "memes"
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true)
item, err := r.GetById(ctx, 1) item, err := r.GetById(ctx, 1)
if err != nil { if err != nil {
@ -238,7 +238,7 @@ func TestDiscordWebHookSoftDelete(t *testing.T) {
serverName := "Unit Testing" serverName := "Unit Testing"
channel := "memes" channel := "memes"
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true)
_, err = r.SoftDelete(ctx, 1) _, err = r.SoftDelete(ctx, 1)
if err != nil { if err != nil {
t.Log(err) t.Log(err)
@ -263,7 +263,7 @@ func TestDiscordWebHookRestore(t *testing.T) {
timeZero := time.Time{} timeZero := time.Time{}
r := repository.NewDiscordWebHookRepository(db) r := repository.NewDiscordWebHookRepository(db)
_, _ = r.Create(ctx, "www.discord.com/bad/webhook", serverName, channel, true) _, _ = r.Create(ctx, 999, "www.discord.com/bad/webhook", serverName, channel, true)
item, _ := r.GetById(ctx, 1) item, _ := r.GetById(ctx, 1)
if item.DeletedAt != timeZero { if item.DeletedAt != timeZero {
t.Log("DeletedAt was not zero") t.Log("DeletedAt was not zero")

View File

@ -1,12 +1,13 @@
package repository package repository
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
) )
@ -15,9 +16,9 @@ const (
) )
type RefreshToken interface { type RefreshToken interface {
Create(username string, token string) (int64, error) Create(ctx context.Context, username string, token string) (int64, error)
GetByUsername(name string) (domain.RefreshTokenEntity, error) GetByUsername(ctx context.Context, name string) (entity.RefreshTokenEntity, error)
DeleteById(id int64) (int64, error) DeleteById(ctx context.Context, id int64) (int64, error)
} }
type RefreshTokenRepository struct { type RefreshTokenRepository struct {
@ -30,15 +31,15 @@ func NewRefreshTokenRepository(conn *sql.DB) RefreshTokenRepository {
} }
} }
func (rt RefreshTokenRepository) Create(username string, token string) (int64, error) { func (rt RefreshTokenRepository) Create(ctx context.Context, username string, token string) (int64, error) {
dt := time.Now() dt := time.Now()
builder := sqlbuilder.NewInsertBuilder() builder := sqlbuilder.NewInsertBuilder()
builder.InsertInto(refreshTokenTableName) builder.InsertInto(refreshTokenTableName)
builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt") builder.Cols("Username", "Token", "CreatedAt", "UpdatedAt", "DeletedAt")
builder.Values(username, token, dt, dt) builder.Values(username, token, dt, dt, time.Time{})
query, args := builder.Build() query, args := builder.Build()
_, err := rt.connection.Exec(query, args...) _, err := rt.connection.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -46,27 +47,27 @@ func (rt RefreshTokenRepository) Create(username string, token string) (int64, e
return 1, nil return 1, nil
} }
func (rt RefreshTokenRepository) GetByUsername(name string) (domain.RefreshTokenEntity, error) { func (rt RefreshTokenRepository) GetByUsername(ctx context.Context, name string) (entity.RefreshTokenEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*").From(refreshTokenTableName).Where( builder.Select("*").From(refreshTokenTableName).Where(
builder.E("Username", name), builder.E("Username", name),
) )
query, args := builder.Build() query, args := builder.Build()
rows, err := rt.connection.Query(query, args...) rows, err := rt.connection.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.RefreshTokenEntity{}, err return entity.RefreshTokenEntity{}, err
} }
data := rt.processRows(rows) data := rt.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.RefreshTokenEntity{}, errors.New("no token found for user") return entity.RefreshTokenEntity{}, errors.New("no token found for user")
} }
return data[0], nil return data[0], nil
} }
func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) { func (rt RefreshTokenRepository) DeleteById(ctx context.Context, id int64) (int64, error) {
builder := sqlbuilder.NewDeleteBuilder() builder := sqlbuilder.NewDeleteBuilder()
builder.DeleteFrom(refreshTokenTableName) builder.DeleteFrom(refreshTokenTableName)
builder.Where( builder.Where(
@ -74,7 +75,7 @@ func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) {
) )
query, args := builder.Build() query, args := builder.Build()
rows, err := rt.connection.Exec(query, args...) rows, err := rt.connection.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
return -1, err return -1, err
} }
@ -82,8 +83,8 @@ func (rt RefreshTokenRepository) DeleteById(id int64) (int64, error) {
return rows.RowsAffected() return rows.RowsAffected()
} }
func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTokenEntity { func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []entity.RefreshTokenEntity {
items := []domain.RefreshTokenEntity{} items := []entity.RefreshTokenEntity{}
for rows.Next() { for rows.Next() {
var id int64 var id int64
@ -98,7 +99,7 @@ func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTok
fmt.Println(err) fmt.Println(err)
} }
item := domain.RefreshTokenEntity{ item := entity.RefreshTokenEntity{
ID: id, ID: id,
Username: username, Username: username,
Token: token, Token: token,
@ -106,7 +107,7 @@ func (rd RefreshTokenRepository) processRows(rows *sql.Rows) []domain.RefreshTok
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
} }
if (deletedAt.Valid) { if deletedAt.Valid {
item.DeletedAt = deletedAt.Time item.DeletedAt = deletedAt.Time
} }

View File

@ -1,6 +1,7 @@
package repository_test package repository_test
import ( import (
"context"
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
@ -14,7 +15,7 @@ func TestRefreshTokenCreate(t *testing.T) {
} }
client := repository.NewRefreshTokenRepository(conn) client := repository.NewRefreshTokenRepository(conn)
rows, err := client.Create("tester", "BadTokenDontUse") rows, err := client.Create(context.Background(), "tester", "BadTokenDontUse")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -33,7 +34,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) {
} }
client := repository.NewRefreshTokenRepository(conn) client := repository.NewRefreshTokenRepository(conn)
rows, err := client.Create("tester", "BadTokenDoNotUse") rows, err := client.Create(context.Background(), "tester", "BadTokenDoNotUse")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -44,7 +45,7 @@ func TestRefreshTokenGetByUsername(t *testing.T) {
t.FailNow() t.FailNow()
} }
model, err := client.GetByUsername("tester") model, err := client.GetByUsername(context.Background(), "tester")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -64,7 +65,7 @@ func TestRefreshTokenDeleteById(t *testing.T) {
} }
client := repository.NewRefreshTokenRepository(conn) client := repository.NewRefreshTokenRepository(conn)
created, err := client.Create("tester", "BadTokenDoNotUse") created, err := client.Create(context.Background(), "tester", "BadTokenDoNotUse")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -73,13 +74,13 @@ func TestRefreshTokenDeleteById(t *testing.T) {
t.Log("Unexpected number back for rows created") t.Log("Unexpected number back for rows created")
} }
model, err := client.GetByUsername("tester") model, err := client.GetByUsername(context.Background(), "tester")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
} }
updated, err := client.DeleteById(model.ID) updated, err := client.DeleteById(context.Background(), model.ID)
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()

View File

@ -5,18 +5,18 @@ import (
"database/sql" "database/sql"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
) )
type Sources interface { type Sources interface {
Create(ctx context.Context, source, displayName, url, tags string, enabled bool) (int64, error) Create(ctx context.Context, source, displayName, url, tags string, enabled bool) (int64, error)
GetById(ctx context.Context, id int64) (domain.SourceEntity, error) GetById(ctx context.Context, id int64) (entity.SourceEntity, error)
GetByDisplayName(ctx context.Context, displayName string) (domain.SourceEntity, error) GetByDisplayName(ctx context.Context, displayName string) (entity.SourceEntity, error)
GetBySource(ctx context.Context, source string) (domain.SourceEntity, error) GetBySource(ctx context.Context, source string) (entity.SourceEntity, error)
GetBySourceAndName(ctx context.Context, source, name string) (domain.SourceEntity, error) GetBySourceAndName(ctx context.Context, source, name string) (entity.SourceEntity, error)
List(ctx context.Context, page, limit int) ([]domain.SourceEntity, error) List(ctx context.Context, page, limit int) ([]entity.SourceEntity, error)
ListBySource(ctx context.Context, page, limit int, source string) ([]domain.SourceEntity, error) ListBySource(ctx context.Context, page, limit int, source string) ([]entity.SourceEntity, error)
Enable(ctx context.Context, id int64) (int64, error) Enable(ctx context.Context, id int64) (int64, error)
Disable(ctx context.Context, id int64) (int64, error) Disable(ctx context.Context, id int64) (int64, error)
SoftDelete(ctx context.Context, id int64) (int64, error) SoftDelete(ctx context.Context, id int64) (int64, error)
@ -50,7 +50,7 @@ func (r sourceRepository) Create(ctx context.Context, source, displayName, url,
return 1, nil return 1, nil
} }
func (r sourceRepository) GetById(ctx context.Context, id int64) (domain.SourceEntity, error) { func (r sourceRepository) GetById(ctx context.Context, id int64) (entity.SourceEntity, error) {
b := sqlbuilder.NewSelectBuilder() b := sqlbuilder.NewSelectBuilder()
b.Select("*") b.Select("*")
b.From("Sources").Where( b.From("Sources").Where(
@ -61,18 +61,18 @@ func (r sourceRepository) GetById(ctx context.Context, id int64) (domain.SourceE
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
return data[0], nil return data[0], nil
} }
func (r sourceRepository) GetByDisplayName(ctx context.Context, displayName string) (domain.SourceEntity, error) { func (r sourceRepository) GetByDisplayName(ctx context.Context, displayName string) (entity.SourceEntity, error) {
b := sqlbuilder.NewSelectBuilder() b := sqlbuilder.NewSelectBuilder()
b.Select("*") b.Select("*")
b.From("Sources").Where( b.From("Sources").Where(
@ -83,18 +83,18 @@ func (r sourceRepository) GetByDisplayName(ctx context.Context, displayName stri
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
return data[0], nil return data[0], nil
} }
func (r sourceRepository) GetBySource(ctx context.Context, source string) (domain.SourceEntity, error) { func (r sourceRepository) GetBySource(ctx context.Context, source string) (entity.SourceEntity, error) {
b := sqlbuilder.NewSelectBuilder() b := sqlbuilder.NewSelectBuilder()
b.Select("*") b.Select("*")
b.From("Sources").Where( b.From("Sources").Where(
@ -105,41 +105,41 @@ func (r sourceRepository) GetBySource(ctx context.Context, source string) (domai
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
return data[0], nil return data[0], nil
} }
func (r sourceRepository) GetBySourceAndName(ctx context.Context, source, name string) (domain.SourceEntity, error) { func (r sourceRepository) GetBySourceAndName(ctx context.Context, source, name string) (entity.SourceEntity, error) {
b := sqlbuilder.NewSelectBuilder() b := sqlbuilder.NewSelectBuilder()
b.Select("*") b.Select("*")
b.From("Sources").Where( b.From("Sources").Where(
b.Equal("Source", source), b.Equal("Source", source),
b.Equal("Name", name), b.Equal("DisplayName", name),
) )
b.Limit(1) b.Limit(1)
query, args := b.Build() query, args := b.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.SourceEntity{}, err return entity.SourceEntity{}, err
} }
return data[0], nil return data[0], nil
} }
func (r sourceRepository) List(ctx context.Context, page, limit int) ([]domain.SourceEntity, error) { func (r sourceRepository) List(ctx context.Context, page, limit int) ([]entity.SourceEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("Sources") builder.From("Sources")
@ -149,18 +149,18 @@ func (r sourceRepository) List(ctx context.Context, page, limit int) ([]domain.S
query, args := builder.Build() query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.SourceEntity{}, err return []entity.SourceEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.SourceEntity{}, err return []entity.SourceEntity{}, err
} }
return data, nil return data, nil
} }
func (r sourceRepository) ListBySource(ctx context.Context, page, limit int, source string) ([]domain.SourceEntity, error) { func (r sourceRepository) ListBySource(ctx context.Context, page, limit int, source string) ([]entity.SourceEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*") builder.Select("*")
builder.From("Sources") builder.From("Sources")
@ -173,12 +173,12 @@ func (r sourceRepository) ListBySource(ctx context.Context, page, limit int, sou
query, args := builder.Build() query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...) rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return []domain.SourceEntity{}, err return []entity.SourceEntity{}, err
} }
data, err := r.processRows(rows) data, err := r.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return []domain.SourceEntity{}, err return []entity.SourceEntity{}, err
} }
return data, nil return data, nil
@ -236,8 +236,8 @@ func (r sourceRepository) Delete(ctx context.Context, id int64) (int64, error) {
return deleteFromTable(ctx, r.conn, "Sources", id) return deleteFromTable(ctx, r.conn, "Sources", id)
} }
func (r sourceRepository) processRows(rows *sql.Rows) ([]domain.SourceEntity, error) { func (r sourceRepository) processRows(rows *sql.Rows) ([]entity.SourceEntity, error) {
items := []domain.SourceEntity{} items := []entity.SourceEntity{}
for rows.Next() { for rows.Next() {
var id int64 var id int64
@ -258,7 +258,7 @@ func (r sourceRepository) processRows(rows *sql.Rows) ([]domain.SourceEntity, er
return items, err return items, err
} }
item := domain.SourceEntity{ item := entity.SourceEntity{
ID: id, ID: id,
CreatedAt: createdAt, CreatedAt: createdAt,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,

View File

@ -4,7 +4,7 @@ import (
"context" "context"
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
) )

View File

@ -0,0 +1,120 @@
package repository
import (
"context"
"database/sql"
"errors"
"fmt"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder"
)
type UserSourceRepo interface {
Create(ctx context.Context, userId, sourceId int64) (int64, error)
SoftDelete(ctx context.Context, id int64) (int64, error)
Restore(ctx context.Context, id int64) (int64, error)
Delete(ctx context.Context, id int64) (int64, error)
ListByUser(ctx context.Context, page, limit int, userId int64) ([]entity.UserSourceSubscriptionEntity, error)
}
type userSourceRepository struct {
conn *sql.DB
defaultLimit int
defaultOffset int
}
func NewUserSourceRepository(conn *sql.DB) userSourceRepository {
return userSourceRepository{
conn: conn,
defaultLimit: 50,
defaultOffset: 50,
}
}
func (r userSourceRepository) Create(ctx context.Context, userId, sourceId int64) (int64, error) {
dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("UserSourceSubscriptions")
queryBuilder.Cols("UpdatedAt", "CreatedAt", "DeletedAt", "UserID", "SourceID")
queryBuilder.Values(dt, dt, timeZero, userId, sourceId)
query, args := queryBuilder.Build()
_, err := r.conn.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
return 1, nil
}
func (r userSourceRepository) SoftDelete(ctx context.Context, id int64) (int64, error) {
return softDeleteRow(ctx, r.conn, "UserSourceSubscriptions", id)
}
func (r userSourceRepository) Restore(ctx context.Context, id int64) (int64, error) {
return restoreRow(ctx, r.conn, "UserSourceSubscriptions", id)
}
func (r userSourceRepository) Delete(ctx context.Context, id int64) (int64, error) {
return deleteFromTable(ctx, r.conn, "UserSourceSubscriptions", id)
}
func (r userSourceRepository) ListByUser(ctx context.Context, page, limit int, userId int64) ([]entity.UserSourceSubscriptionEntity, error) {
builder := sqlbuilder.NewSelectBuilder()
builder.Select("*")
builder.From("UserSourceSubscriptions")
builder.Where(
builder.Equal("UserID", userId),
)
builder.Offset(page * limit)
builder.Limit(limit)
query, args := builder.Build()
rows, err := r.conn.QueryContext(ctx, query, args...)
if err != nil {
return []entity.UserSourceSubscriptionEntity{}, err
}
data := r.processRows(rows)
if len(data) == 0 {
return []entity.UserSourceSubscriptionEntity{}, errors.New(ErrUserNotFound)
}
return data, nil
}
func (ur userSourceRepository) processRows(rows *sql.Rows) []entity.UserSourceSubscriptionEntity {
items := []entity.UserSourceSubscriptionEntity{}
for rows.Next() {
var id int64
var createdAt time.Time
var updatedAt time.Time
var deletedAt time.Time
var userId int64
var sourceId int64
err := rows.Scan(
&id, &createdAt, &updatedAt, &deletedAt,
&userId, &sourceId,
)
if err != nil {
fmt.Println(err)
}
item := entity.UserSourceSubscriptionEntity{
ID: id,
CreatedAt: createdAt,
UpdatedAt: updatedAt,
DeletedAt: deletedAt,
UserID: userId,
SourceID: sourceId,
}
items = append(items, item)
}
return items
}

View File

@ -1,29 +1,31 @@
package repository package repository
import ( import (
"context"
"database/sql" "database/sql"
"errors" "errors"
"fmt" "fmt"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"github.com/huandu/go-sqlbuilder" "github.com/huandu/go-sqlbuilder"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
) )
const ( const (
TableName string = "users" usersTableName string = "users"
ErrUserNotFound string = "requested user was not found" ErrUserNotFound string = "requested user was not found"
) )
type Users interface { type Users interface {
GetByName(name string) (domain.UserEntity, error) GetByName(ctx context.Context, name string) (entity.UserEntity, error)
Create(name, password, scope string) (int64, error) Create(ctx context.Context, name, password, sessionTOken, scope string) (int64, error)
Update(id int, entity domain.UserEntity) error Update(ctx context.Context, id int, entity entity.UserEntity) error
UpdatePassword(name, password string) error UpdatePassword(ctx context.Context, name, password string) error
CheckUserHash(name, password string) error CheckUserHash(ctx context.Context, name, password string) error
UpdateScopes(name, scope string) error UpdateScopes(ctx context.Context, name, scope string) error
UpdateSessionToken(ctx context.Context, name, sessionToken string) (int64, error)
} }
// Creates a new instance of UserRepository with the bound sql // Creates a new instance of UserRepository with the bound sql
@ -37,27 +39,27 @@ type userRepository struct {
connection *sql.DB connection *sql.DB
} }
func (ur userRepository) GetByName(name string) (domain.UserEntity, error) { func (ur userRepository) GetByName(ctx context.Context, name string) (entity.UserEntity, error) {
builder := sqlbuilder.NewSelectBuilder() builder := sqlbuilder.NewSelectBuilder()
builder.Select("*").From("users").Where( builder.Select("*").From("users").Where(
builder.E("Name", name), builder.E("Name", name),
) )
query, args := builder.Build() query, args := builder.Build()
rows, err := ur.connection.Query(query, args...) rows, err := ur.connection.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return domain.UserEntity{}, err return entity.UserEntity{}, err
} }
data := ur.processRows(rows) data := ur.processRows(rows)
if len(data) == 0 { if len(data) == 0 {
return domain.UserEntity{}, errors.New(ErrUserNotFound) return entity.UserEntity{}, errors.New(ErrUserNotFound)
} }
return data[0], nil return data[0], nil
} }
func (ur userRepository) Create(name, password, scope string) (int64, error) { func (ur userRepository) Create(ctx context.Context, name, password, sessionToken, scope string) (int64, error) {
passwordBytes := []byte(password) passwordBytes := []byte(password)
hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost) hash, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
if err != nil { if err != nil {
@ -67,11 +69,11 @@ func (ur userRepository) Create(name, password, scope string) (int64, error) {
dt := time.Now() dt := time.Now()
queryBuilder := sqlbuilder.NewInsertBuilder() queryBuilder := sqlbuilder.NewInsertBuilder()
queryBuilder.InsertInto("users") queryBuilder.InsertInto("users")
queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "Scopes") queryBuilder.Cols("Name", "Hash", "UpdatedAt", "CreatedAt", "DeletedAt", "Scopes", "SessionToken")
queryBuilder.Values(name, string(hash), dt, dt, scope) queryBuilder.Values(name, string(hash), dt, dt, time.Time{}, scope, sessionToken)
query, args := queryBuilder.Build() query, args := queryBuilder.Build()
_, err = ur.connection.Exec(query, args...) _, err = ur.connection.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
return 0, err return 0, err
} }
@ -79,26 +81,50 @@ func (ur userRepository) Create(name, password, scope string) (int64, error) {
return 1, nil return 1, nil
} }
func (ur userRepository) Update(id int, entity domain.UserEntity) error { func (ur userRepository) Update(ctx context.Context, id int, entity entity.UserEntity) error {
return errors.New("not implemented") return errors.New("not implemented")
} }
func (ur userRepository) UpdatePassword(name, password string) error { func (ur userRepository) UpdatePassword(ctx context.Context, name, password string) error {
_, err := ur.GetByName(name) _, err := ur.GetByName(ctx, name)
if err != nil { if err != nil {
return nil return nil
} }
queryBuilder := sqlbuilder.NewUpdateBuilder() queryBuilder := sqlbuilder.NewUpdateBuilder()
queryBuilder.Update(TableName) queryBuilder.Update(usersTableName)
//queryBuilder.Set //queryBuilder.Set
return nil return nil
} }
func (ur userRepository) UpdateSessionToken(ctx context.Context, name, sessionToken string) (int64, error) {
_, err := ur.GetByName(ctx, name)
if err != nil {
return 0, err
}
q := sqlbuilder.NewUpdateBuilder()
q.Update(usersTableName)
q.Set(
q.Equal("SessionToken", sessionToken),
)
q.Where(
q.Equal("Name", name),
)
query, args := q.Build()
rowsUpdates, err := ur.connection.ExecContext(ctx, query, args...)
if err != nil {
return 0, err
}
return rowsUpdates.RowsAffected()
}
// If the hash matches what we have in the database, an error will not be returned. // If the hash matches what we have in the database, an error will not be returned.
// If the user does not exist or the hash does not match, an error will be returned // If the user does not exist or the hash does not match, an error will be returned
func (ur userRepository) CheckUserHash(name, password string) error { func (ur userRepository) CheckUserHash(ctx context.Context, name, password string) error {
record, err := ur.GetByName(name) record, err := ur.GetByName(ctx, name)
if err != nil { if err != nil {
return err return err
} }
@ -111,7 +137,7 @@ func (ur userRepository) CheckUserHash(name, password string) error {
return nil return nil
} }
func (ur userRepository) UpdateScopes(name, scope string) error { func (ur userRepository) UpdateScopes(ctx context.Context, name, scope string) error {
builder := sqlbuilder.NewUpdateBuilder() builder := sqlbuilder.NewUpdateBuilder()
builder.Update("users") builder.Update("users")
builder.Set( builder.Set(
@ -122,15 +148,15 @@ func (ur userRepository) UpdateScopes(name, scope string) error {
) )
query, args := builder.Build() query, args := builder.Build()
_, err := ur.connection.Exec(query, args...) _, err := ur.connection.ExecContext(ctx, query, args...)
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (ur userRepository) processRows(rows *sql.Rows) []domain.UserEntity { func (ur userRepository) processRows(rows *sql.Rows) []entity.UserEntity {
items := []domain.UserEntity{} items := []entity.UserEntity{}
for rows.Next() { for rows.Next() {
var id int64 var id int64
@ -140,18 +166,20 @@ func (ur userRepository) processRows(rows *sql.Rows) []domain.UserEntity {
var updatedAt time.Time var updatedAt time.Time
var deletedAt sql.NullTime var deletedAt sql.NullTime
var scopes string var scopes string
err := rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &username, &hash, &scopes) var sessionToken string
err := rows.Scan(&id, &createdAt, &updatedAt, &deletedAt, &username, &hash, &scopes, &sessionToken)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
} }
item := domain.UserEntity{ item := entity.UserEntity{
ID: id, ID: id,
UpdatedAt: updatedAt, UpdatedAt: updatedAt,
Username: username, Username: username,
Hash: hash, Hash: hash,
Scopes: scopes, Scopes: scopes,
CreatedAt: createdAt, CreatedAt: createdAt,
SessionToken: sessionToken,
} }
if deletedAt.Valid { if deletedAt.Valid {
item.DeletedAt = deletedAt.Time item.DeletedAt = deletedAt.Time

View File

@ -1,6 +1,7 @@
package repository_test package repository_test
import ( import (
"context"
"database/sql" "database/sql"
"log" "log"
"testing" "testing"
@ -20,7 +21,7 @@ func TestCanCreateNewUser(t *testing.T) {
defer db.Close() defer db.Close()
repo := repository.NewUserRepository(db) repo := repository.NewUserRepository(db)
updated, err := repo.Create("testing", "NotSecure", "placeholder") updated, err := repo.Create(context.Background(), "testing", "NotSecure", "sessionToken", "placeholder")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
t.FailNow() t.FailNow()
@ -37,7 +38,7 @@ func TestCanFindUserInTable(t *testing.T) {
defer db.Close() defer db.Close()
repo := repository.NewUserRepository(db) repo := repository.NewUserRepository(db)
updated, err := repo.Create("testing", "NotSecure", "placeholder") updated, err := repo.Create(context.Background(), "testing", "NotSecure", "sessionToken", "placeholder")
if err != nil { if err != nil {
t.Log(err) t.Log(err)
t.FailNow() t.FailNow()
@ -48,7 +49,7 @@ func TestCanFindUserInTable(t *testing.T) {
t.FailNow() t.FailNow()
} }
user, err := repo.GetByName("testing") user, err := repo.GetByName(context.Background(), "testing")
if err != nil { if err != nil {
log.Println(err) log.Println(err)
t.FailNow() t.FailNow()
@ -65,7 +66,7 @@ func TestCheckUserHash(t *testing.T) {
defer db.Close() defer db.Close()
repo := repository.NewUserRepository(db) repo := repository.NewUserRepository(db)
repo.CheckUserHash("testing", "NotSecure") repo.CheckUserHash(context.Background(), "testing", "NotSecure")
} }
func setupInMemoryDb() (*sql.DB, error) { func setupInMemoryDb() (*sql.DB, error) {

View File

@ -0,0 +1,87 @@
package repositoryservices
import (
"context"
"database/sql"
"errors"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
"github.com/google/uuid"
)
const (
ErrUnexpectedAmountOfRowsUpdated = "got a unexpected of rows updated"
)
type RefreshToken interface {
Create(ctx context.Context, username string) (string, error)
GetByName(ctx context.Context, name string) (entity.RefreshTokenEntity, error)
Delete(ctx context.Context, id int64) (int64, error)
IsRequestValid(ctx context.Context, username, refreshToken string) error
}
// A new jwt token can be made if the user has the correct refresh token for the user.
// It will also require the old JWT token so the expire time is pulled and part of the validation
type RefreshTokenService struct {
table repository.RefreshTokenRepository
}
func NewRefreshTokenService(conn *sql.DB) RefreshTokenService {
return RefreshTokenService{
table: repository.NewRefreshTokenRepository(conn),
}
}
func (rt RefreshTokenService) Create(ctx context.Context, username string) (string, error) {
//if a refresh token already exists for a user, reuse
existingToken, err := rt.GetByName(ctx, username)
if err == nil {
rowsRemoved, err := rt.Delete(ctx, existingToken.ID)
if err != nil {
return "", err
}
if rowsRemoved != 1 {
return "", errors.New(ErrUnexpectedAmountOfRowsUpdated)
}
}
token, err := uuid.NewV7()
if err != nil {
return "", err
}
rows, err := rt.table.Create(ctx, username, token.String())
if err != nil {
return "", err
}
if rows != 1 {
return "", errors.New("expected one row but got none")
}
return token.String(), nil
}
// Find the saved refresh token for a user and return it if it exists
func (rt RefreshTokenService) GetByName(ctx context.Context, name string) (entity.RefreshTokenEntity, error) {
return rt.table.GetByUsername(ctx, name)
}
// This will request that a object is removed from the database
func (rt RefreshTokenService) Delete(ctx context.Context, id int64) (int64, error) {
return rt.table.DeleteById(ctx, id)
}
func (rt RefreshTokenService) IsRequestValid(ctx context.Context, username, refreshToken string) error {
token, err := rt.GetByName(ctx, username)
if err != nil {
return err
}
if token.Token != refreshToken {
return errors.New("the refresh token given does not match")
}
return nil
}

View File

@ -0,0 +1,198 @@
package repositoryservices
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
"github.com/google/uuid"
"golang.org/x/crypto/bcrypt"
)
const (
ErrPasswordNotLongEnough = "password needs to be 8 character or longer"
ErrPasswordMissingSpecialCharacter = "password needs to contain one of the following: !, @, #"
ErrInvalidPassword = "invalid password"
)
type UserServices interface {
DoesUserExist(ctx context.Context, username string) error
DoesPasswordMatchHash(ctx context.Context, username, password string) error
GetUser(ctx context.Context, username string) (entity.UserEntity, error)
AddScopes(ctx context.Context, username string, scopes []string) error
RemoveScopes(ctx context.Context, username string, scopes []string) error
Create(ctx context.Context, name, password, scope string) (entity.UserEntity, error)
NewSessionToken(ctx context.Context, name string) (string, error)
CheckPasswordForRequirements(password string) error
}
// This will handle operations that are user related, but one layer higher then the repository
type UserService struct {
repo repository.Users
}
// This is a layer on top of the Users Repository.
// Use this over directly talking to the table when ever possible.
func NewUserService(conn *sql.DB) UserService {
return UserService{
repo: repository.NewUserRepository(conn),
}
}
func (us UserService) DoesUserExist(ctx context.Context, username string) error {
_, err := us.repo.GetByName(ctx, username)
if err != nil {
return err
}
return nil
}
func (us UserService) DoesPasswordMatchHash(ctx context.Context, username, password string) error {
model, err := us.GetUser(ctx, username)
if err != nil {
return err
}
err = bcrypt.CompareHashAndPassword([]byte(model.Hash), []byte(password))
if err != nil {
return errors.New(ErrInvalidPassword)
}
return nil
}
func (us UserService) GetUser(ctx context.Context, username string) (entity.UserEntity, error) {
return us.repo.GetByName(ctx, username)
}
func (us UserService) AddScopes(ctx context.Context, username string, scopes []string) error {
usr, err := us.repo.GetByName(ctx, username)
if err != nil {
return err
}
if usr.Username != username {
return errors.New(repository.ErrUserNotFound)
}
currentScopes := strings.Split(usr.Scopes, ",")
// check the current scopes
for _, item := range scopes {
if !strings.Contains(usr.Scopes, item) {
currentScopes = append(currentScopes, item)
}
}
return us.repo.UpdateScopes(ctx, username, strings.Join(currentScopes, ","))
}
func (us UserService) RemoveScopes(ctx context.Context, username string, scopes []string) error {
usr, err := us.repo.GetByName(ctx, username)
if err != nil {
return err
}
if usr.Username != username {
return errors.New(repository.ErrUserNotFound)
}
var newScopes []string
// check all the scopes that are currently assigned
for _, item := range strings.Split(usr.Scopes, ",") {
// check the scopes given, if one matches skip it
if us.doesScopeExist(scopes, item) {
continue
}
// did not match, add it
newScopes = append(newScopes, item)
}
return us.repo.UpdateScopes(ctx, username, strings.Join(newScopes, ","))
}
func (us UserService) doesScopeExist(scopes []string, target string) bool {
for _, item := range scopes {
if item == target {
return true
}
}
return false
}
func (us UserService) Create(ctx context.Context, name, password, scope string) (entity.UserEntity, error) {
err := us.CheckPasswordForRequirements(password)
if err != nil {
return entity.UserEntity{}, err
}
token, err := uuid.NewV7()
if err != nil {
return entity.UserEntity{}, err
}
us.repo.Create(ctx, name, password, token.String(), domain.ScopeArticleRead)
return entity.UserEntity{}, nil
}
func (us UserService) NewSessionToken(ctx context.Context, name string) (string, error) {
token, err := uuid.NewV7()
if err != nil {
return "", err
}
rows, err := us.repo.UpdateSessionToken(ctx, name, token.String())
if err != nil {
return "", err
}
if rows != 1 {
return "", fmt.Errorf("UserService.NewSessionToken %w", err)
}
return token.String(), nil
}
func (us UserService) CheckPasswordForRequirements(password string) error {
err := us.checkPasswordLength(password)
if err != nil {
return err
}
err = us.checkPasswordForSpecialCharacters(password)
if err != nil {
return err
}
return nil
}
func (us UserService) checkPasswordLength(password string) error {
if len(password) < 8 {
return errors.New(ErrPasswordNotLongEnough)
}
return nil
}
func (us UserService) checkPasswordForSpecialCharacters(password string) error {
var chars []string
chars = append(chars, "!")
chars = append(chars, "@")
chars = append(chars, "#")
for _, char := range chars {
if strings.Contains(password, char) {
return nil
}
}
return errors.New(ErrPasswordMissingSpecialCharacter)
}

View File

@ -11,9 +11,9 @@ import (
) )
const ( const (
ServerAddress = "SERVER_ADDRESS" ServerAddress = "ServerAddress"
Sql_Connection_String = "SQL_CONNECTION_STRING" //Sql_Connection_String = "SQL_CONNECTION_STRING"
FEATURE_ENABLE_REDDIT_BACKEND = "FEATURE_ENABLE_REDDIT_BACKEND" FEATURE_ENABLE_REDDIT_BACKEND = "FEATURE_ENABLE_REDDIT_BACKEND"
REDDIT_PULL_TOP = "REDDIT_PULL_TOP" REDDIT_PULL_TOP = "REDDIT_PULL_TOP"
@ -34,6 +34,8 @@ const (
type Configs struct { type Configs struct {
ServerAddress string ServerAddress string
JwtSecret string
AdminSecret string
RedditEnabled bool RedditEnabled bool
RedditPullTop bool RedditPullTop bool
@ -64,6 +66,8 @@ func NewConfig() ConfigClient {
func GetEnvConfig() Configs { func GetEnvConfig() Configs {
return Configs{ return Configs{
ServerAddress: os.Getenv(ServerAddress), ServerAddress: os.Getenv(ServerAddress),
JwtSecret: os.Getenv("JwtSecret"),
AdminSecret: os.Getenv("AdminSecret"),
RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)), RedditEnabled: processBoolConfig(os.Getenv(FEATURE_ENABLE_REDDIT_BACKEND)),
RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)), RedditPullTop: processBoolConfig(os.Getenv(REDDIT_PULL_TOP)),

View File

@ -0,0 +1,220 @@
package cron
import (
"log"
"time"
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
)
func (c *Cron) CollectRssPosts() {
log.Println("Starting ")
sources, err := c.repo.Sources.ListBySource(c.ctx, 0, 1000, domain.SourceCollectorRss)
if err != nil {
log.Println(err)
}
for sourceIndex, source := range sources {
if !source.Enabled {
continue
}
rssClient := input.NewRssClient(source)
articles, err := rssClient.GetArticles()
if err != nil {
log.Println(err)
}
for _, article := range articles {
_, err := c.repo.Articles.GetByUrl(c.ctx, article.Url)
if err == nil {
continue
}
rowsCreated, err := c.repo.Articles.CreateFromEntity(c.ctx, article)
if err != nil {
log.Println(err)
}
if rowsCreated != 1 {
log.Println("Got back the wrong number of rows")
}
}
if sourceIndex != len(sources) {
time.Sleep(time.Second * 30)
}
}
}
func (c *Cron) CollectRedditPosts() {
sources, err := c.repo.Sources.ListBySource(c.ctx, 0, 1000, domain.SourceCollectorReddit)
if err != nil {
log.Printf("[Reddit] No sources found to query - %v\r", err)
}
for _, source := range sources {
if !source.Enabled {
continue
}
log.Printf("[Reddit] Checking '%v'...", source.DisplayName)
rc := input.NewRedditClient(source)
raw, err := rc.GetContent()
if err != nil {
log.Println(err)
}
redditArticles := rc.ConvertToArticles(raw)
for _, article := range redditArticles {
_, err := c.repo.Articles.GetByUrl(c.ctx, article.Url)
if err == nil {
continue
}
rowsAdded, err := c.repo.Articles.CreateFromEntity(c.ctx, article)
if err != nil {
log.Printf("Failed to add a new reddit article to the database: %s", err)
}
if rowsAdded != 1 {
log.Printf("no error came back when data was added to the database but the expected row count is wrong")
}
}
}
log.Print("[Reddit] Done!")
}
func (c *Cron) CollectYoutubePosts() {
sources, err := c.repo.Sources.ListBySource(c.ctx, 0, 1000, domain.SourceCollectorYoutube)
if err != nil {
log.Printf("[Youtube] No sources found to query - %v\r", err)
}
for sourceIndex, source := range sources {
if !source.Enabled {
continue
}
log.Printf("[YouTube] Checking '%v'...", source.DisplayName)
yc := input.NewYoutubeClient(source)
raw, err := yc.GetContent()
if err != nil {
log.Println(err)
}
for _, article := range raw {
_, err := c.repo.Articles.GetByUrl(c.ctx, article.Url)
if err == nil {
continue
}
rowsAdded, err := c.repo.Articles.CreateFromEntity(c.ctx, article)
if err != nil {
log.Printf("Failed to add a new youtube article to the database: %s", err)
}
if rowsAdded != 1 {
log.Printf("no error came back when data was added to the database but the expected row count is wrong")
}
}
if sourceIndex != len(sources) {
time.Sleep(time.Second * 30)
}
}
log.Print("[YouTube] Done!")
}
func (c *Cron) CollectFfxivPosts() {
sources, err := c.repo.Sources.ListBySource(c.ctx, 0, 1000, domain.SourceCollectorFfxiv)
if err != nil {
log.Printf("[FFXIV] No sources found to query - %v\r", err)
}
for sourceIndex, source := range sources {
if !source.Enabled {
continue
}
fc := input.NewFFXIVClient(source)
items, err := fc.CheckSource()
if err != nil {
log.Println(err)
}
for _, article := range items {
_, err := c.repo.Articles.GetByUrl(c.ctx, article.Url)
if err == nil {
continue
}
rowsAdded, err := c.repo.Articles.CreateFromEntity(c.ctx, article)
if err != nil {
log.Printf("Failed to add a new FFXIV article to the database: %s", err)
}
if rowsAdded != 1 {
log.Printf("no error came back when data was added to the database but the expected row count is wrong")
}
}
if sourceIndex != len(sources) {
time.Sleep(time.Second * 30)
}
}
log.Printf("[FFXIV Done!]")
}
func (c *Cron) CollectTwitchPosts() {
sources, err := c.repo.Sources.ListBySource(c.ctx, 0, 1000, domain.SourceCollectorTwitch)
if err != nil {
log.Printf("[Twitch] No sources found to query - %v\r", err)
}
tc, err := input.NewTwitchClient()
if err != nil {
log.Println(err)
return
}
err = tc.Login()
if err != nil {
log.Println(err)
}
for sourceIndex, source := range sources {
if !source.Enabled {
continue
}
log.Printf("[Twitch] Checking '%v'...", source.DisplayName)
tc.ReplaceSourceRecord(source)
items, err := tc.GetContent()
if err != nil {
log.Println(err)
}
for _, article := range items {
_, err := c.repo.Articles.GetByUrl(c.ctx, article.Url)
if err == nil {
continue
}
rowsAdded, err := c.repo.Articles.CreateFromEntity(c.ctx, article)
if err != nil {
log.Printf("Failed to add a new Twitch article to the database: %s", err)
}
if rowsAdded != 1 {
log.Printf("no error came back when data was added to the database but the expected row count is wrong")
}
}
if sourceIndex != len(sources) {
time.Sleep(time.Second * 30)
}
}
log.Print("[Twitch] Done!")
}

View File

@ -0,0 +1,43 @@
package cron_test
import (
"context"
"testing"
"git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/cron"
)
func TestRssPullsCorrectly(t *testing.T) {
conn, err := setupInMemoryDb()
if err != nil {
t.Error(err)
t.FailNow()
}
defer conn.Close()
ctx := context.Background()
db := services.NewRepositoryService(conn)
rowsCreated, err := db.Sources.Create(ctx, domain.SourceCollectorRss, "Gitea - Newsbot.api", "https://git.jamestombleson.com/jtom38/newsbot-api.rss", "rss,gitea,newsbot.api", true)
if err != nil {
t.Error(err)
t.FailNow()
}
if rowsCreated != 1 {
t.Error("failed to create the source record")
t.FailNow()
}
client := cron.NewScheduler(ctx, conn)
client.CollectRssPosts()
articles, err := db.Articles.ListByPage(ctx, 0, 100)
if err != nil {
t.Error(err)
t.FailNow()
}
t.Log(len(articles))
}

View File

@ -3,83 +3,37 @@ package cron
import ( import (
"context" "context"
"database/sql" "database/sql"
"fmt"
"log"
"time"
"github.com/google/uuid"
_ "github.com/lib/pq" _ "github.com/lib/pq"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" //"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/output"
) )
type Cron struct { type Cron struct {
Db *database.Queries //Db *database.Queries
ctx *context.Context ctx context.Context
timer *cron.Cron timer *cron.Cron
repo services.RepositoryService
} }
func openDatabase() (*database.Queries, error) { func NewScheduler(ctx context.Context, conn *sql.DB) *Cron {
_env := services.NewConfig()
connString := _env.GetConfig(services.Sql_Connection_String)
if connString == "" {
panic("Connection String is null!")
}
db, err := sql.Open("postgres", connString)
if err != nil {
panic(err)
}
queries := database.New(db)
return queries, err
}
func NewScheduler(ctx context.Context) *Cron {
c := &Cron{ c := &Cron{
ctx: &ctx, ctx: ctx,
repo: services.NewRepositoryService(conn),
} }
timer := cron.New() timer := cron.New()
queries, err := openDatabase()
if err != nil {
panic(err)
}
c.Db = queries
//timer.AddFunc("*/5 * * * *", func() { go CheckCache() }) //timer.AddFunc("*/5 * * * *", func() { go CheckCache() })
features := services.NewConfig() //features := services.GetEnvConfig()
res, _ := features.GetFeature(services.FEATURE_ENABLE_REDDIT_BACKEND) timer.AddFunc("5 * * * *", func() { go c.CollectRssPosts() })
if res { //timer.AddFunc("10 * * * *", c.CollectRedditPosts)
timer.AddFunc("5 1-23 * * *", func() { go c.CheckReddit() }) //timer.AddFunc("15 * * * *", c.CheckYoutube)
log.Print("[Input] Reddit backend was enabled") //timer.AddFunc("20 * * * *", c.CheckFfxiv)
//go c.CheckReddit() //timer.AddFunc("25 * * * *", c.CheckTwitch)
} //timer.AddFunc("*/5 * * * *", c.CheckDiscordQueue)
res, _ = features.GetFeature(services.FEATURE_ENABLE_YOUTUBE_BACKEND)
if res {
timer.AddFunc("10 1-23 * * *", func() { go c.CheckYoutube() })
log.Print("[Input] YouTube backend was enabled")
}
res, _ = features.GetFeature(services.FEATURE_ENABLE_FFXIV_BACKEND)
if res {
timer.AddFunc("5 5,10,15,20 * * *", func() { go c.CheckFfxiv() })
log.Print("[Input] FFXIV backend was enabled")
}
res, _ = features.GetFeature(services.FEATURE_ENABLE_TWITCH_BACKEND)
if res {
timer.AddFunc("15 1-23 * * *", func() { go c.CheckTwitch() })
log.Print("[Input] Twitch backend was enabled")
}
timer.AddFunc("*/5 * * * *", func() { go c.CheckDiscordQueue() })
log.Print("[Output] Discord Output was enabled")
c.timer = timer c.timer = timer
return c return c
@ -93,105 +47,8 @@ func (c *Cron) Stop() {
c.timer.Stop() c.timer.Stop()
} }
// This is the main entry point to query all the reddit services /*
func (c *Cron) CheckReddit() { func (c *Cron) CheckDiscordQueue() {
sources, err := c.Db.ListSourcesBySource(*c.ctx, "reddit")
if err != nil {
log.Printf("[Reddit] No sources found to query - %v\r", err)
}
for _, source := range sources {
if !source.Enabled {
continue
}
log.Printf("[Reddit] Checking '%v'...", source.Name)
rc := input.NewRedditClient(source)
raw, err := rc.GetContent()
if err != nil {
log.Println(err)
}
redditArticles := rc.ConvertToArticles(raw)
c.checkPosts(redditArticles, "Reddit")
}
log.Print("[Reddit] Done!")
}
func (c *Cron) CheckYoutube() {
// Add call to the db to request youtube sources.
sources, err := c.Db.ListSourcesBySource(*c.ctx, "youtube")
if err != nil {
log.Printf("[Youtube] No sources found to query - %v\r", err)
}
for _, source := range sources {
if !source.Enabled {
continue
}
log.Printf("[YouTube] Checking '%v'...", source.Name)
yc := input.NewYoutubeClient(source)
raw, err := yc.GetContent()
if err != nil {
log.Println(err)
}
c.checkPosts(raw, "YouTube")
}
log.Print("[YouTube] Done!")
}
func (c *Cron) CheckFfxiv() {
sources, err := c.Db.ListSourcesBySource(*c.ctx, "ffxiv")
if err != nil {
log.Printf("[FFXIV] No sources found to query - %v\r", err)
}
for _, source := range sources {
if !source.Enabled {
continue
}
fc := input.NewFFXIVClient(source)
items, err := fc.CheckSource()
if err != nil {
log.Println(err)
}
c.checkPosts(items, "FFXIV")
}
log.Printf("[FFXIV Done!]")
}
func (c *Cron) CheckTwitch() error {
sources, err := c.Db.ListSourcesBySource(*c.ctx, "twitch")
if err != nil {
log.Printf("[Twitch] No sources found to query - %v\r", err)
}
tc, err := input.NewTwitchClient()
if err != nil {
return err
}
err = tc.Login()
if err != nil {
return err
}
for _, source := range sources {
if !source.Enabled {
continue
}
log.Printf("[Twitch] Checking '%v'...", source.Name)
tc.ReplaceSourceRecord(source)
items, err := tc.GetContent()
if err != nil {
log.Println(err)
}
c.checkPosts(items, "Twitch")
}
log.Print("[Twitch] Done!")
return nil
}
func (c *Cron) CheckDiscordQueue() error {
// Get items from the table // Get items from the table
queueItems, err := c.Db.ListDiscordQueueItems(*c.ctx, 50) queueItems, err := c.Db.ListDiscordQueueItems(*c.ctx, 50)
if err != nil { if err != nil {
@ -260,55 +117,15 @@ func (c *Cron) CheckDiscordQueue() error {
return nil return nil
} }
*/
func (c *Cron) checkPosts(posts []database.Article, sourceName string) error { //func (c *Cron) addToDiscordQueue(Id uuid.UUID) error {
for _, item := range posts { // err := c.Db.CreateDiscordQueue(*c.ctx, database.CreateDiscordQueueParams{
_, err := c.Db.GetArticleByUrl(*c.ctx, item.Url) // ID: uuid.New(),
if err != nil { // Articleid: Id,
id := uuid.New() // })
// if err != nil {
err := c.postArticle(id, item) // return err
if err != nil { // }
return fmt.Errorf("[%v] Failed to post article - %v - %v.\r", sourceName, item.Url, err) // return nil
} //}
err = c.addToDiscordQueue(id)
if err != nil {
return err
}
}
}
time.Sleep(30 * time.Second)
return nil
}
func (c *Cron) postArticle(id uuid.UUID, item database.Article) error {
err := c.Db.CreateArticle(*c.ctx, database.CreateArticleParams{
ID: id,
Sourceid: item.Sourceid,
Tags: item.Tags,
Title: item.Title,
Url: item.Url,
Pubdate: item.Pubdate,
Video: item.Video,
Videoheight: item.Videoheight,
Videowidth: item.Videowidth,
Thumbnail: item.Thumbnail,
Description: item.Description,
Authorname: item.Authorname,
Authorimage: item.Authorimage,
})
return err
}
func (c *Cron) addToDiscordQueue(Id uuid.UUID) error {
err := c.Db.CreateDiscordQueue(*c.ctx, database.CreateDiscordQueueParams{
ID: uuid.New(),
Articleid: Id,
})
if err != nil {
return err
}
return nil
}

View File

@ -1,12 +1,12 @@
package cron_test package cron_test
import ( import (
"context" "database/sql"
"testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/cron" "github.com/pressly/goose/v3"
) )
/*
func TestInvokeTwitch(t *testing.T) { func TestInvokeTwitch(t *testing.T) {
} }
@ -15,7 +15,7 @@ func TestInvokeTwitch(t *testing.T) {
func TestCheckReddit(t *testing.T) { func TestCheckReddit(t *testing.T) {
ctx := context.Background() ctx := context.Background()
c := cron.NewScheduler(ctx) c := cron.NewScheduler(ctx)
c.CheckReddit() c.Col()
} }
func TestCheckYouTube(t *testing.T) { func TestCheckYouTube(t *testing.T) {
@ -32,3 +32,22 @@ func TestCheckTwitch(t *testing.T) {
t.Error(err) t.Error(err)
} }
} }
*/
func setupInMemoryDb() (*sql.DB, error) {
db, err := sql.Open("sqlite", ":memory:")
if err != nil {
return nil, err
}
err = goose.SetDialect("sqlite3")
if err != nil {
return nil, err
}
err = goose.Up(db, "../../database/migrations")
if err != nil {
return nil, err
}
return db, nil
}

View File

@ -4,22 +4,27 @@ import (
"database/sql" "database/sql"
"git.jamestombleson.com/jtom38/newsbot-api/internal/repository" "git.jamestombleson.com/jtom38/newsbot-api/internal/repository"
repositoryservices "git.jamestombleson.com/jtom38/newsbot-api/internal/repositoryServices"
) )
type RepositoryService struct { type RepositoryService struct {
AlertDiscord repository.AlertDiscordRepo
Articles repository.ArticlesRepo Articles repository.ArticlesRepo
DiscordWebHooks repository.DiscordWebHookRepo DiscordWebHooks repository.DiscordWebHookRepo
RefreshTokens repositoryservices.RefreshToken
Sources repository.Sources Sources repository.Sources
Users repository.Users Users repositoryservices.UserServices
RefreshTokens repository.RefreshToken UserSourceSubscriptions repository.UserSourceRepo
} }
func NewRepositoryService(conn *sql.DB) RepositoryService { func NewRepositoryService(conn *sql.DB) RepositoryService {
return RepositoryService{ return RepositoryService{
AlertDiscord: repository.NewAlertDiscordRepository(conn),
Articles: repository.NewArticleRepository(conn), Articles: repository.NewArticleRepository(conn),
DiscordWebHooks: repository.NewDiscordWebHookRepository(conn), DiscordWebHooks: repository.NewDiscordWebHookRepository(conn),
RefreshTokens: repositoryservices.NewRefreshTokenService(conn),
Sources: repository.NewSourceRepository(conn), Sources: repository.NewSourceRepository(conn),
Users: repository.NewUserRepository(conn), Users: repositoryservices.NewUserService(conn),
RefreshTokens: repository.NewRefreshTokenRepository(conn), UserSourceSubscriptions: repository.NewUserSourceRepository(conn),
} }
} }

View File

@ -1,140 +0,0 @@
// The converter package lives between the database calls and the API calls.
// This way if any new methods like RPC calls are added later, the API does not need to be reworked as much
package dto
import (
"context"
"strings"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
"github.com/google/uuid"
)
type DtoClient struct {
db *database.Queries
}
func NewDtoClient(db *database.Queries) *DtoClient {
return &DtoClient{
db: db,
}
}
func (c *DtoClient) ListArticles(ctx context.Context, limit, page int) ([]models.ArticleDto, error) {
var res []models.ArticleDto
a, err := c.db.ListArticles(ctx, database.ListArticlesParams{
Limit: int32(limit),
Offset: int32(limit * page),
})
if err != nil {
return res, err
}
for _, article := range a {
res = append(res, c.convertArticle(article))
}
return res, nil
}
func (c *DtoClient) ListArticlesByPage(ctx context.Context, page, limit int32) ([]models.ArticleDto, error) {
var res []models.ArticleDto
a, err := c.db.ListArticlesByPage(ctx, database.ListArticlesByPageParams{
Limit: limit,
Offset: page * limit,
})
if err != nil {
return res, err
}
for _, article := range a {
res = append(res, c.convertArticle(article))
}
return res, nil
}
func (c *DtoClient) GetArticle(ctx context.Context, ID uuid.UUID) (models.ArticleDto, error) {
a, err := c.db.GetArticleByID(ctx, ID)
if err != nil {
return models.ArticleDto{}, err
}
return c.convertArticle(a), nil
}
func (c *DtoClient) GetArticleDetails(ctx context.Context, ID uuid.UUID) (models.ArticleDetailsDto, error) {
a, err := c.db.GetArticleByID(ctx, ID)
if err != nil {
return models.ArticleDetailsDto{}, err
}
s, err := c.db.GetSourceByID(ctx, a.Sourceid)
if err != nil {
return models.ArticleDetailsDto{}, err
}
res := c.convertArticleDetails(a, s)
return res, nil
}
func (c *DtoClient) ListNewArticlesBySourceId(ctx context.Context, SourceID uuid.UUID, limit, page int) ([]models.ArticleDto, error) {
var res []models.ArticleDto
a, err := c.db.ListNewArticlesBySourceId(ctx, database.ListNewArticlesBySourceIdParams{
Sourceid: SourceID,
Limit: int32(limit),
Offset: int32(limit * page),
})
if err != nil {
return res, err
}
for _, article := range a {
res = append(res, c.convertArticle(article))
}
return res, nil
}
func (c *DtoClient) convertArticle(i database.Article) models.ArticleDto {
return models.ArticleDto{
ID: i.ID,
Source: i.Sourceid,
Tags: c.SplitTags(i.Tags),
Title: i.Title,
Url: i.Url,
Pubdate: i.Pubdate,
Video: i.Video.String,
Videoheight: i.Videoheight,
Videowidth: i.Videoheight,
Thumbnail: i.Thumbnail,
Description: i.Description,
Authorname: i.Authorname.String,
Authorimage: i.Authorimage.String,
}
}
func (c *DtoClient) convertArticleDetails(i database.Article, s database.Source) models.ArticleDetailsDto {
return models.ArticleDetailsDto{
ID: i.ID,
Source: c.ConvertToSource(s),
Tags: c.SplitTags(i.Tags),
Title: i.Title,
Url: i.Url,
Pubdate: i.Pubdate,
Video: i.Video.String,
Videoheight: i.Videoheight,
Videowidth: i.Videoheight,
Thumbnail: i.Thumbnail,
Description: i.Description,
Authorname: i.Authorname.String,
Authorimage: i.Authorimage.String,
}
}
func (c DtoClient) SplitTags(t string) []string {
return strings.Split(t, ", ")
}

View File

@ -1,63 +0,0 @@
package dto
import (
"context"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
"github.com/google/uuid"
)
func (c *DtoClient) ListDiscordWebHooks(ctx context.Context, total int32) ([]models.DiscordWebHooksDto, error) {
var res []models.DiscordWebHooksDto
items, err := c.db.ListDiscordWebhooks(ctx, total)
if err != nil {
return res, nil
}
for _, item := range items {
res = append(res, c.ConvertDiscordWebhook(item))
}
return res, nil
}
func (c *DtoClient) GetDiscordWebhook(ctx context.Context, id uuid.UUID) (models.DiscordWebHooksDto, error) {
var res models.DiscordWebHooksDto
item, err := c.db.GetDiscordWebHooksByID(ctx, id)
if err != nil {
return res, err
}
return c.ConvertDiscordWebhook(item), nil
}
func (c *DtoClient) GetDiscordWebHookByServerAndChannel(ctx context.Context, server, channel string) ([]models.DiscordWebHooksDto, error) {
var res []models.DiscordWebHooksDto
items, err := c.db.GetDiscordWebHooksByServerAndChannel(ctx, database.GetDiscordWebHooksByServerAndChannelParams{
Server: server,
Channel: channel,
})
if err != nil {
return res, err
}
for _, item := range items {
res = append(res, c.ConvertDiscordWebhook(item))
}
return res, nil
}
func (c *DtoClient) ConvertDiscordWebhook(i database.Discordwebhook) models.DiscordWebHooksDto {
return models.DiscordWebHooksDto{
ID: i.ID,
Url: i.Url,
Server: i.Server,
Channel: i.Channel,
Enabled: i.Enabled,
}
}

View File

@ -1,42 +0,0 @@
package dto
import (
"context"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
)
func (c *DtoClient) ListDiscordWebhookQueue(ctx context.Context, limit int32) {
}
func (c *DtoClient) ListDiscordWebhookQueueDetails(ctx context.Context, limit int32) ([]models.DiscordQueueDetailsDto, error) {
var res []models.DiscordQueueDetailsDto
items, err := c.db.ListDiscordQueueItems(ctx, limit)
if err != nil {
return res, err
}
for _, item := range items {
article, err := c.GetArticleDetails(ctx, item.ID)
if err != nil {
return res, err
}
res = append(res, models.DiscordQueueDetailsDto{
ID: item.ID,
Article: article,
})
}
return res, nil
}
func (c *DtoClient) ConvertToDiscordQueueDto(i database.Discordqueue) models.DiscordQueueDto {
return models.DiscordQueueDto{
ID: i.ID,
Articleid: i.Articleid,
}
}

View File

@ -1,85 +0,0 @@
package dto
import (
"context"
"strings"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
"github.com/google/uuid"
)
func (c *DtoClient) ListSources(ctx context.Context, limit int32) ([]models.SourceDto, error) {
var res []models.SourceDto
items, err := c.db.ListSources(ctx, limit)
if err != nil {
return res, err
}
for _, item := range items {
res = append(res, c.ConvertToSource(item))
}
return res, nil
}
func (c *DtoClient) ListSourcesBySource(ctx context.Context, sourceName string) ([]models.SourceDto, error) {
var res []models.SourceDto
items, err := c.db.ListSourcesBySource(ctx, strings.ToLower(sourceName))
if err != nil {
return res, err
}
for _, item := range items {
res = append(res, c.ConvertToSource(item))
}
return res, nil
}
func (c *DtoClient) GetSourceById(ctx context.Context, id uuid.UUID) (models.SourceDto, error) {
var res models.SourceDto
item, err := c.db.GetSourceByID(ctx, id)
if err != nil {
return res, err
}
return c.ConvertToSource(item), nil
}
func (c *DtoClient) GetSourceByNameAndSource(ctx context.Context, name, source string) (models.SourceDto, error) {
var res models.SourceDto
item, err := c.db.GetSourceByNameAndSource(ctx, database.GetSourceByNameAndSourceParams{
Name: name,
Source: source,
})
if err != nil {
return res, err
}
return c.ConvertToSource(item), nil
}
func (c *DtoClient) ConvertToSource(i database.Source) models.SourceDto {
var deleted bool
if !i.Deleted.Valid {
deleted = true
}
return models.SourceDto{
ID: i.ID,
Site: i.Site,
Name: i.Name,
Source: i.Source,
Type: i.Type,
Value: i.Value.String,
Enabled: i.Enabled,
Url: i.Url,
Tags: c.SplitTags(i.Tags),
Deleted: deleted,
}
}

View File

@ -1,91 +0,0 @@
package dto
import (
"context"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain/models"
"github.com/google/uuid"
)
func (c *DtoClient) ListSubscriptions(ctx context.Context, limit int32) ([]models.SubscriptionDto, error) {
var res []models.SubscriptionDto
items, err := c.db.ListSubscriptions(ctx, limit)
if err != nil {
return res, err
}
for _, item := range items {
res = append(res, c.ConvertSubscription(item))
}
return res, nil
}
func (c *DtoClient) ListSubscriptionDetails(ctx context.Context, limit int32) ([]models.SubscriptionDetailsDto, error) {
var res []models.SubscriptionDetailsDto
items, err := c.ListSubscriptions(ctx, limit)
if err != nil {
return res, err
}
for _, item := range items {
dwh, err := c.GetDiscordWebhook(ctx, item.DiscordWebhookId)
if err != nil {
return res, err
}
source, err := c.GetSourceById(ctx, item.SourceId)
if err != nil {
return res, err
}
res = append(res, models.SubscriptionDetailsDto{
ID: item.ID,
Source: source,
DiscordWebHook: dwh,
})
}
return res, nil
}
func (c *DtoClient) ListSubscriptionsByDiscordWebhookId(ctx context.Context, id uuid.UUID) ([]models.SubscriptionDto, error) {
var res []models.SubscriptionDto
items, err := c.db.GetSubscriptionsByDiscordWebHookId(ctx, id)
if err != nil {
return res, err
}
for _, item := range items {
res = append(res, c.ConvertSubscription(item))
}
return res, nil
}
func (c *DtoClient) ListSubscriptionsBySourceId(ctx context.Context, id uuid.UUID) ([]models.SubscriptionDto, error) {
var res []models.SubscriptionDto
items, err := c.db.GetSubscriptionsBySourceID(ctx, id)
if err != nil {
return res, err
}
for _, item := range items {
res = append(res, c.ConvertSubscription(item))
}
return res, nil
}
func (c *DtoClient) ConvertSubscription(i database.Subscription) models.SubscriptionDto {
return models.SubscriptionDto{
ID: i.ID,
DiscordWebhookId: i.Discordwebhookid,
SourceId: i.Sourceid,
}
}

View File

@ -1,7 +1,6 @@
package input package input
import ( import (
"database/sql"
"errors" "errors"
"log" "log"
"net/http" "net/http"
@ -13,7 +12,7 @@ import (
"github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/launcher"
"github.com/google/uuid" "github.com/google/uuid"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/cache" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/cache"
) )
@ -25,7 +24,7 @@ const (
) )
type FFXIVClient struct { type FFXIVClient struct {
record database.Source record entity.SourceEntity
//SourceID uint //SourceID uint
//Url string //Url string
//Region string //Region string
@ -33,15 +32,15 @@ type FFXIVClient struct {
cacheGroup string cacheGroup string
} }
func NewFFXIVClient(Record database.Source) FFXIVClient { func NewFFXIVClient(Record entity.SourceEntity) FFXIVClient {
return FFXIVClient{ return FFXIVClient{
record: Record, record: Record,
cacheGroup: "ffxiv", cacheGroup: "ffxiv",
} }
} }
func (fc *FFXIVClient) CheckSource() ([]database.Article, error) { func (fc *FFXIVClient) CheckSource() ([]entity.ArticleEntity, error) {
var articles []database.Article var articles []entity.ArticleEntity
parser := fc.GetBrowser() parser := fc.GetBrowser()
defer parser.Close() defer parser.Close()
@ -97,18 +96,16 @@ func (fc *FFXIVClient) CheckSource() ([]database.Article, error) {
return articles, err return articles, err
} }
article := database.Article{ article := entity.ArticleEntity{
Sourceid: fc.record.ID, SourceID: fc.record.ID,
Tags: tags, Tags: tags,
Title: title, Title: title,
Url: link, Url: link,
Pubdate: pubDate, PubDate: pubDate,
Videoheight: 0,
Videowidth: 0,
Thumbnail: thumb, Thumbnail: thumb,
Description: description, Description: description,
Authorname: sql.NullString{String: authorName}, AuthorName: authorName,
Authorimage: sql.NullString{String: authorImage}, AuthorImageUrl: authorImage,
} }
log.Printf("Collected '%v' from '%v'", article.Title, article.Url) log.Printf("Collected '%v' from '%v'", article.Title, article.Url)

View File

@ -3,16 +3,15 @@ package input_test
import ( import (
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
ffxiv "git.jamestombleson.com/jtom38/newsbot-api/internal/services/input" ffxiv "git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
"github.com/google/uuid"
) )
var FFXIVRecord database.Source = database.Source{ var FFXIVRecord entity.SourceEntity = entity.SourceEntity{
ID: uuid.New(), ID: 9999,
Site: "ffxiv", DisplayName: "Final Fantasy XIV - NA",
Name: "Final Fantasy XIV - NA", Source: domain.SourceCollectorFfxiv,
Source: "ffxiv",
Url: "https://na.finalfantasyxiv.com/lodestone/", Url: "https://na.finalfantasyxiv.com/lodestone/",
Tags: "ffxiv, final, fantasy, xiv, na, lodestone", Tags: "ffxiv, final, fantasy, xiv, na, lodestone",
} }

View File

@ -2,7 +2,7 @@ package input
import ( import (
"crypto/tls" "crypto/tls"
"io/ioutil" "io"
"log" "log"
"net/http" "net/http"
) )
@ -35,7 +35,7 @@ func getHttpContent(uri string) ([]byte, error) {
} }
defer resp.Body.Close() defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -1,7 +1,6 @@
package input package input
import ( import (
"database/sql"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -9,8 +8,8 @@ import (
"strings" "strings"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"github.com/go-rod/rod" "github.com/go-rod/rod"
"github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/launcher"
@ -18,7 +17,7 @@ import (
type RedditClient struct { type RedditClient struct {
config RedditConfig config RedditConfig
record database.Source record entity.SourceEntity
} }
type RedditConfig struct { type RedditConfig struct {
@ -27,7 +26,7 @@ type RedditConfig struct {
PullNSFW string PullNSFW string
} }
func NewRedditClient(Record database.Source) *RedditClient { func NewRedditClient(Record entity.SourceEntity) *RedditClient {
rc := RedditClient{ rc := RedditClient{
record: Record, record: Record,
} }
@ -71,7 +70,7 @@ func (rc *RedditClient) GetContent() (domain.RedditJsonContent, error) {
// TODO Wire this to support the config options // TODO Wire this to support the config options
Url := fmt.Sprintf("%v.json", rc.record.Url) Url := fmt.Sprintf("%v.json", rc.record.Url)
log.Printf("[Reddit] Collecting results on '%v'", rc.record.Name) log.Printf("[Reddit] Collecting results on '%v'", rc.record.DisplayName)
content, err := getHttpContent(Url) content, err := getHttpContent(Url)
if err != nil { if err != nil {
@ -88,10 +87,10 @@ func (rc *RedditClient) GetContent() (domain.RedditJsonContent, error) {
return items, nil return items, nil
} }
func (rc *RedditClient) ConvertToArticles(items domain.RedditJsonContent) []database.Article { func (rc *RedditClient) ConvertToArticles(items domain.RedditJsonContent) []entity.ArticleEntity {
var redditArticles []database.Article var redditArticles []entity.ArticleEntity
for _, item := range items.Data.Children { for _, item := range items.Data.Children {
var article database.Article var article entity.ArticleEntity
article, err := rc.convertToArticle(item.Data) article, err := rc.convertToArticle(item.Data)
if err != nil { if err != nil {
log.Printf("[Reddit] %v", err) log.Printf("[Reddit] %v", err)
@ -104,8 +103,8 @@ func (rc *RedditClient) ConvertToArticles(items domain.RedditJsonContent) []data
// ConvertToArticle() will take the reddit model struct and convert them over to Article structs. // ConvertToArticle() will take the reddit model struct and convert them over to Article structs.
// This data can be passed to the database. // This data can be passed to the database.
func (rc *RedditClient) convertToArticle(source domain.RedditPost) (database.Article, error) { func (rc *RedditClient) convertToArticle(source domain.RedditPost) (entity.ArticleEntity, error) {
var item database.Article var item entity.ArticleEntity
if source.Content == "" && source.Url != "" { if source.Content == "" && source.Url != "" {
item = rc.convertPicturePost(source) item = rc.convertPicturePost(source)
@ -131,65 +130,57 @@ func (rc *RedditClient) convertToArticle(source domain.RedditPost) (database.Art
return item, nil return item, nil
} }
func (rc *RedditClient) convertPicturePost(source domain.RedditPost) database.Article { func (rc *RedditClient) convertPicturePost(source domain.RedditPost) entity.ArticleEntity {
var item = database.Article{ var item = entity.ArticleEntity{
Sourceid: rc.record.ID, SourceID: rc.record.ID,
Title: source.Title, Title: source.Title,
Tags: fmt.Sprintf("%v", rc.record.Tags), Tags: fmt.Sprintf("%v", rc.record.Tags),
Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink), Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink),
Pubdate: time.Now(), PubDate: time.Now(),
Video: sql.NullString{String: "null"}, IsVideo: false,
Videoheight: 0,
Videowidth: 0,
Thumbnail: source.Thumbnail, Thumbnail: source.Thumbnail,
Description: source.Content, Description: source.Content,
Authorname: sql.NullString{String: source.Author}, AuthorName: source.Author,
Authorimage: sql.NullString{String: "null"}, AuthorImageUrl: "null",
} }
return item return item
} }
func (rc *RedditClient) convertTextPost(source domain.RedditPost) database.Article { func (rc *RedditClient) convertTextPost(source domain.RedditPost) entity.ArticleEntity {
var item = database.Article{ var item = entity.ArticleEntity{
Sourceid: rc.record.ID, SourceID: rc.record.ID,
Tags: "a", Tags: "a",
Title: source.Title, Title: source.Title,
Pubdate: time.Now(), PubDate: time.Now(),
Videoheight: 0,
Videowidth: 0,
Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink), Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink),
Authorname: sql.NullString{String: source.Author}, AuthorName: source.Author,
Description: source.Content, Description: source.Content,
} }
return item return item
} }
func (rc *RedditClient) convertVideoPost(source domain.RedditPost) database.Article { func (rc *RedditClient) convertVideoPost(source domain.RedditPost) entity.ArticleEntity {
var item = database.Article{ var item = entity.ArticleEntity{
Sourceid: rc.record.ID, SourceID: rc.record.ID,
Tags: "a", Tags: "a",
Title: source.Title, Title: source.Title,
Pubdate: time.Now(), PubDate: time.Now(),
Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink), Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink),
Videoheight: 0, AuthorName: source.Author,
Videowidth: 0,
Authorname: sql.NullString{String: source.Author},
Description: source.Media.RedditVideo.FallBackUrl, Description: source.Media.RedditVideo.FallBackUrl,
} }
return item return item
} }
// This post is nothing more then a redirect to another location. // This post is nothing more then a redirect to another location.
func (rc *RedditClient) convertRedirectPost(source domain.RedditPost) database.Article { func (rc *RedditClient) convertRedirectPost(source domain.RedditPost) entity.ArticleEntity {
var item = database.Article{ var item = entity.ArticleEntity{
Sourceid: rc.record.ID, SourceID: rc.record.ID,
Tags: "a", Tags: "a",
Title: source.Title, Title: source.Title,
Pubdate: time.Now(), PubDate: time.Now(),
Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink), Url: fmt.Sprintf("https://www.reddit.com%v", source.Permalink),
Videoheight: 0, AuthorName: source.Author,
Videowidth: 0,
Authorname: sql.NullString{String: source.Author},
Description: source.UrlOverriddenByDest, Description: source.UrlOverriddenByDest,
} }
return item return item

View File

@ -3,16 +3,15 @@ package input_test
import ( import (
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/input" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
"github.com/google/uuid"
) )
var RedditRecord database.Source = database.Source{ var RedditRecord entity.SourceEntity = entity.SourceEntity{
ID: uuid.New(), ID: 9999,
Name: "dadjokes", DisplayName: "dadjokes",
Source: "reddit", Source: domain.SourceCollectorRss,
Site: "reddit",
Url: "https://reddit.com/r/dadjokes", Url: "https://reddit.com/r/dadjokes",
Tags: "reddit, dadjokes", Tags: "reddit, dadjokes",
} }

View File

@ -1,19 +1,21 @@
package input package input
import ( import (
"fmt" "strings"
"log"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/cache"
"github.com/mmcdole/gofeed" "github.com/mmcdole/gofeed"
) )
type rssClient struct { type FeedInput interface {
SourceRecord domain.SourceEntity GetArticles() (entity.ArticleEntity, error)
} }
func NewRssClient(sourceRecord domain.SourceEntity) rssClient { type rssClient struct {
SourceRecord entity.SourceEntity
}
func NewRssClient(sourceRecord entity.SourceEntity) rssClient {
client := rssClient{ client := rssClient{
SourceRecord: sourceRecord, SourceRecord: sourceRecord,
} }
@ -21,39 +23,55 @@ func NewRssClient(sourceRecord domain.SourceEntity) rssClient {
return client return client
} }
//func (rc rssClient) ReplaceSourceRecord(source model.Sources) { func (rc rssClient) GetArticles() ([]entity.ArticleEntity, error) {
//rc.SourceRecord = source parser := gofeed.NewParser()
//} feed, err := parser.ParseURL(rc.SourceRecord.Url)
func (rc rssClient) getCacheGroup() string {
return fmt.Sprintf("rss-%v", rc.SourceRecord.DisplayName)
}
func (rc rssClient) GetContent() error {
feed, err := rc.PullFeed()
if err != nil {
return err
}
cacheClient := cache.NewCacheClient(rc.getCacheGroup())
for _, item := range feed.Items {
log.Println(item)
cacheClient.FindByValue(item.Link)
}
return nil
}
func (rc rssClient) PullFeed() (*gofeed.Feed, error) {
feedUri := fmt.Sprintf("%v", rc.SourceRecord.Url)
fp := gofeed.NewParser()
feed, err := fp.ParseURL(feedUri)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return feed, nil sourceTags := strings.Split(rc.SourceRecord.Tags, ",")
var articles []entity.ArticleEntity
for _, post := range feed.Items {
article := entity.ArticleEntity{
SourceID: rc.SourceRecord.ID,
Title: post.Title,
Description: post.Content,
Url: post.Link,
PubDate: *post.PublishedParsed,
//AuthorName: post.Authors[0].Email,
}
if len(post.Authors) != 0 {
article.AuthorName = post.Authors[0].Email
}
var postTags []string
postTags = append(postTags, sourceTags...)
postTags = append(postTags, post.Categories...)
article.Tags = strings.Join(postTags, ",")
/*
pageContent, err := getHttpContent(article.Url)
if err != nil {
continue
}
htmlNode, err := html.Parse(bytes.NewReader(pageContent))
if err != nil {
continue
}
htmlNode.
fmt.Println(htmlNode)
*/
if post.Image == nil {
article.Thumbnail = ""
}
articles = append(articles, article)
}
return articles, nil
} }

View File

@ -3,14 +3,16 @@ package input_test
import ( import (
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/domain" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/input" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
) )
var rssRecord = domain.SourceEntity{ var rssRecord = entity.SourceEntity{
ID: 1, ID: 1,
DisplayName: "ArsTechnica", DisplayName: "ArsTechnica",
Url: "https://feeds.arstechnica.com/arstechnica/index", Url: "https://feeds.arstechnica.com/arstechnica/index",
Source: domain.SourceCollectorRss,
} }
func TestRssClientConstructor(t *testing.T) { func TestRssClientConstructor(t *testing.T) {
@ -19,12 +21,23 @@ func TestRssClientConstructor(t *testing.T) {
func TestRssGetFeed(t *testing.T) { func TestRssGetFeed(t *testing.T) {
client := input.NewRssClient(rssRecord) client := input.NewRssClient(rssRecord)
feed, err := client.PullFeed() _, err := client.GetArticles()
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
if len(feed.Items) >= 0 { }
t.Error("failed to collect items from the fees")
func TestRssAgainstGita(t *testing.T) {
client := input.NewRssClient(entity.SourceEntity{
ID: 2,
DisplayName: "Gitea - Newsbot-api",
Source: domain.SourceCollectorRss,
Url: "https://git.jamestombleson.com/jtom38/newsbot-api.rss",
Tags: "rss,gitea,newsbot-api",
})
_, err := client.GetArticles()
if err != nil {
t.Error(err)
} }
} }

View File

@ -1,19 +1,18 @@
package input package input
import ( import (
"database/sql"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services" "git.jamestombleson.com/jtom38/newsbot-api/internal/services"
"github.com/nicklaw5/helix/v2" "github.com/nicklaw5/helix/v2"
) )
type TwitchClient struct { type TwitchClient struct {
SourceRecord database.Source SourceRecord entity.SourceEntity
// config // config
monitorClips string monitorClips string
@ -72,7 +71,7 @@ func initTwitchApi(ClientId string, ClientSecret string) (helix.Client, error) {
} }
// This will let you replace the bound source record to keep the same session alive. // This will let you replace the bound source record to keep the same session alive.
func (tc *TwitchClient) ReplaceSourceRecord(source database.Source) { func (tc *TwitchClient) ReplaceSourceRecord(source entity.SourceEntity) {
tc.SourceRecord = source tc.SourceRecord = source
} }
@ -87,8 +86,8 @@ func (tc *TwitchClient) Login() error {
return nil return nil
} }
func (tc *TwitchClient) GetContent() ([]database.Article, error) { func (tc *TwitchClient) GetContent() ([]entity.ArticleEntity, error) {
var items []database.Article var items []entity.ArticleEntity
user, err := tc.GetUserDetails() user, err := tc.GetUserDetails()
if err != nil { if err != nil {
@ -101,31 +100,31 @@ func (tc *TwitchClient) GetContent() ([]database.Article, error) {
} }
for _, video := range posts { for _, video := range posts {
var article database.Article var article entity.ArticleEntity
AuthorName, err := tc.ExtractAuthor(video) AuthorName, err := tc.ExtractAuthor(video)
if err != nil { if err != nil {
return items, err return items, err
} }
article.Authorname = sql.NullString{String: AuthorName} article.AuthorName = AuthorName
Authorimage, err := tc.ExtractAuthorImage(user) Authorimage, err := tc.ExtractAuthorImage(user)
if err != nil { if err != nil {
return items, err return items, err
} }
article.Authorimage = sql.NullString{String: Authorimage} article.AuthorImageUrl = Authorimage
article.Description, err = tc.ExtractDescription(video) article.Description, err = tc.ExtractDescription(video)
if err != nil { if err != nil {
return items, err return items, err
} }
article.Pubdate, err = tc.ExtractPubDate(video) article.PubDate, err = tc.ExtractPubDate(video)
if err != nil { if err != nil {
return items, err return items, err
} }
article.Sourceid = tc.SourceRecord.ID article.SourceID = tc.SourceRecord.ID
article.Tags, err = tc.ExtractTags(video, user) article.Tags, err = tc.ExtractTags(video, user)
if err != nil { if err != nil {
return items, err return items, err
@ -156,7 +155,7 @@ func (tc *TwitchClient) GetUserDetails() (helix.User, error) {
var blank helix.User var blank helix.User
users, err := tc.api.GetUsers(&helix.UsersParams{ users, err := tc.api.GetUsers(&helix.UsersParams{
Logins: []string{tc.SourceRecord.Name}, Logins: []string{tc.SourceRecord.DisplayName},
}) })
if err != nil { if err != nil {
return blank, err return blank, err

View File

@ -4,21 +4,21 @@ import (
"log" "log"
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/input" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
"github.com/google/uuid"
) )
var TwitchSourceRecord = database.Source{ var TwitchSourceRecord = entity.SourceEntity{
ID: uuid.New(), ID: 9999,
Name: "nintendo", DisplayName: "nintendo",
Source: "Twitch", Source: domain.SourceCollectorTwitch,
} }
var TwitchInvalidRecord = database.Source{ var TwitchInvalidRecord = entity.SourceEntity{
ID: uuid.New(), ID: 9999,
Name: "EvilNintendo", DisplayName: "EvilNintendo",
Source: "Twitch", Source: domain.SourceCollectorTwitch,
} }
func TestTwitchLogin(t *testing.T) { func TestTwitchLogin(t *testing.T) {

View File

@ -1,7 +1,6 @@
package input package input
import ( import (
"database/sql"
"errors" "errors"
"fmt" "fmt"
"log" "log"
@ -12,11 +11,11 @@ import (
"github.com/go-rod/rod/lib/launcher" "github.com/go-rod/rod/lib/launcher"
"github.com/mmcdole/gofeed" "github.com/mmcdole/gofeed"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
) )
type YoutubeClient struct { type YoutubeClient struct {
record database.Source record entity.SourceEntity
// internal variables at time of collection // internal variables at time of collection
channelID string channelID string
@ -26,7 +25,7 @@ type YoutubeClient struct {
//debug bool //debug bool
// cache config // cache config
cacheGroup string //cacheGroup string
} }
var ( var (
@ -37,17 +36,16 @@ var (
const YOUTUBE_FEED_URL string = "https://www.youtube.com/feeds/videos.xml?channel_id=" const YOUTUBE_FEED_URL string = "https://www.youtube.com/feeds/videos.xml?channel_id="
func NewYoutubeClient(Record database.Source) YoutubeClient { func NewYoutubeClient(Record entity.SourceEntity) YoutubeClient {
yc := YoutubeClient{ yc := YoutubeClient{
record: Record, record: Record,
cacheGroup: "youtube",
} }
return yc return yc
} }
// CheckSource will go and run all the commands needed to process a source. // CheckSource will go and run all the commands needed to process a source.
func (yc *YoutubeClient) GetContent() ([]database.Article, error) { func (yc *YoutubeClient) GetContent() ([]entity.ArticleEntity, error) {
var items []database.Article var items []entity.ArticleEntity
docParser, err := yc.GetParser(yc.record.Url) docParser, err := yc.GetParser(yc.record.Url)
if err != nil { if err != nil {
return items, err return items, err
@ -247,7 +245,7 @@ func (yc *YoutubeClient) CheckUriCache(uri *string) bool {
return false return false
} }
func (yc *YoutubeClient) ConvertToArticle(item *gofeed.Item) database.Article { func (yc *YoutubeClient) ConvertToArticle(item *gofeed.Item) entity.ArticleEntity {
parser, err := yc.GetParser(item.Link) parser, err := yc.GetParser(item.Link)
if err != nil { if err != nil {
log.Printf("[YouTube] Unable to process %v, submit this link as an issue.\n", item.Link) log.Printf("[YouTube] Unable to process %v, submit this link as an issue.\n", item.Link)
@ -265,16 +263,16 @@ func (yc *YoutubeClient) ConvertToArticle(item *gofeed.Item) database.Article {
log.Printf("[YouTube] %v", msg) log.Printf("[YouTube] %v", msg)
} }
var article = database.Article{ var article = entity.ArticleEntity{
Sourceid: yc.record.ID, SourceID: yc.record.ID,
Tags: tags, Tags: tags,
Title: item.Title, Title: item.Title,
Url: item.Link, Url: item.Link,
Pubdate: *item.PublishedParsed, PubDate: *item.PublishedParsed,
Thumbnail: thumb, Thumbnail: thumb,
Description: item.Description, Description: item.Description,
Authorname: sql.NullString{String: item.Author.Name}, AuthorName: item.Author.Name,
Authorimage: sql.NullString{String: yc.avatarUri}, AuthorImageUrl: yc.avatarUri,
} }
return article return article
} }

View File

@ -3,16 +3,15 @@ package input_test
import ( import (
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/domain"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/input" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/input"
"github.com/google/uuid"
) )
var YouTubeRecord database.Source = database.Source{ var YouTubeRecord = entity.SourceEntity{
ID: uuid.New(), ID: 9999,
Name: "dadjokes", DisplayName: "dadjokes",
Source: "reddit", Source: domain.SourceCollectorReddit,
Site: "reddit",
Url: "https://youtube.com/gamegrumps", Url: "https://youtube.com/gamegrumps",
} }

View File

@ -8,7 +8,8 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" "git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
//"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
) )
type discordField struct { type discordField struct {
@ -63,11 +64,11 @@ const (
type Discord struct { type Discord struct {
Subscriptions []string Subscriptions []string
article database.Article article entity.ArticleEntity
Message *DiscordMessage Message *DiscordMessage
} }
func NewDiscordWebHookMessage(Article database.Article) Discord { func NewDiscordWebHookMessage(Article entity.ArticleEntity) Discord {
return Discord{ return Discord{
article: Article, article: Article,
} }

View File

@ -5,22 +5,19 @@ import (
"strings" "strings"
"testing" "testing"
"git.jamestombleson.com/jtom38/newsbot-api/internal/database" //"git.jamestombleson.com/jtom38/newsbot-api/internal/database"
"git.jamestombleson.com/jtom38/newsbot-api/internal/entity"
"git.jamestombleson.com/jtom38/newsbot-api/internal/services/output" "git.jamestombleson.com/jtom38/newsbot-api/internal/services/output"
"github.com/google/uuid"
"github.com/joho/godotenv" "github.com/joho/godotenv"
) )
var ( var (
article database.Article = database.Article{ article entity.ArticleEntity = entity.ArticleEntity{
ID: uuid.New(), ID: 999,
Sourceid: uuid.New(), SourceID: 1,
Tags: "unit, testing", Tags: "unit, testing",
Title: "Demo", Title: "Demo",
Url: "https://github.com/jtom38/newsbot.collector.api", Url: "https://github.com/jtom38/newsbot.collector.api",
//Pubdate: time.Now(),
Videoheight: 0,
Videowidth: 0,
Description: "Hello World", Description: "Hello World",
} }
blank string = "" blank string = ""

View File

@ -4,22 +4,28 @@ help: ## Shows this help command
build: ## builds the application with the current go runtime build: ## builds the application with the current go runtime
~/go/bin/swag f ~/go/bin/swag f
~/go/bin/swag i ~/go/bin/swag init -g cmd/server.go
go build . go build cmd/server.go
ls -lh server
docker-build: ## Generates the docker image docker-build: ## Generates the docker image
docker build -t "newsbot.collector.api" . docker build -t "newsbot.collector.api" .
docker image ls | grep newsbot.collector.api docker image ls | grep newsbot.collector.api
migrate-dev: ## Apply sql migrations to dev db migrate-dev: ## Apply sql migrations to dev db
goose -dir "./internal/database/migrations" postgres "user=postgres password=postgres dbname=postgres sslmode=disable" up goose -dir "./internal/database/migrations" sqlite3 ./cmd/newsbot.db up
migrate-dev-down: ## revert sql migrations to dev db migrate-dev-down: ## revert sql migrations to dev db
goose -dir "./internal/database/migrations" postgres "user=postgres password=postgres dbname=postgres sslmode=disable" down goose -dir "./internal/database/migrations" sqlite3 ./cmd/newsbot.db down
swag: ## Generates the swagger documentation with the swag tool swag: ## Generates the swagger documentation with the swag tool
~/go/bin/swag f ~/go/bin/swag f -g cmd/server.go
~/go/bin/swag init -g cmd/server.go ~/go/bin/swag init -g cmd/server.go
go run tools/swaggertoopenapi/main.go
oapi-codegen -config api/client.yaml docs/openapi.json
mv api.gen.go api/
gensql: ## Generates SQL code with sqlc install-tools: ## Installs the required tools for this project
sqlc generate go install github.com/swaggo/swag/cmd/swag@v1.8.1
go install github.com/pressly/goose/v3/cmd/goose@latest
go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@v2.3.0

View File

@ -0,0 +1,17 @@
package main
import (
"fmt"
"os"
"git.jamestombleson.com/jtom38/newsbot-api/tools/swaggertoopenapi/src"
)
func main() {
err := src.ConvertToOpenApi("docs/swagger.json", "docs/openapi.json", true)
if err != nil {
fmt.Println(err)
os.Exit(1)
}
}

View File

@ -0,0 +1,70 @@
package src
import (
"bytes"
"fmt"
"io"
"log"
"net/http"
"os"
)
const (
ApplicationJson = "application/json"
)
func ConvertToOpenApi(filePath, outputPath string, force bool) error {
// check if the file already exists
exists, err := os.Stat(outputPath)
if exists != nil {
// if force was given, attempt to remove it
if force {
err = os.Remove(outputPath)
if err != nil {
return err
}
} else {
return fmt.Errorf("'%s' already exists and force was not approved", outputPath)
}
}
log.Printf("Reading '%s'", filePath)
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
client := http.Client{}
req, err := http.NewRequest(http.MethodPost, "https://converter.swagger.io/api/convert", bytes.NewReader(content))
if err != nil {
return err
}
req.Header.Add("Accept", ApplicationJson)
req.Header.Add("Content-Type", ApplicationJson)
log.Println("Converting to OpenAPI spec")
resp, err := client.Do(req)
if err != nil {
return err
}
log.Println("Reading the response")
respContent, err := io.ReadAll(resp.Body)
if err != nil {
return err
}
log.Println("Writing converted data to openapi.json")
writer, err := os.Create(outputPath)
if err != nil {
return err
}
defer writer.Close()
_, err = writer.Write(respContent)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,14 @@
package src_test
import (
"testing"
"git.jamestombleson.com/jtom38/newsbot-api/tools/swaggerToOpenapi/src"
)
func TestConvertOnline(t *testing.T) {
err := src.ConvertToOpenApi("../../docs/swagger.json", "../../docs/openapi.json", true)
if err != nil {
t.FailNow()
}
}