From 713205bb037fc9f74f955821061c5f1bcc98b6e2 Mon Sep 17 00:00:00 2001 From: James Tombleson Date: Sun, 19 Jun 2022 22:02:44 -0700 Subject: [PATCH] Basic routes have been added (#10) * basic routes are working with db context * swagger is working along with swag gen * cron was updated with a class and better db context, untested though * sourcelist command added * lost the pg package but added it back * Updated the api startup for cron and api * updated source routes and started to add article routes * Updated cron add func calls * updated swagger * keeping articles basic for now as I dont need to pull them in yet * swagger update * added getarticlesbysourceid call * adding the subscriptions table to track who to send notifications and where * removed legacy columns from discordwebhooks that are no longer needed. * added discord webhook routes * updated routes * Minor change to schema * Updated routes to support subscriptions * ignore .vscode --- .gitignore | 2 + database/migrations/20220529082459_seed.sql | 2 +- .../20220619085634_subscriptions.sql | 21 + database/models.go | 8 +- database/query.sql.go | 474 ++++++++++++++++- database/schema/query.sql | 58 ++- database/schema/schema.sql | 8 +- docs/docs.go | 483 ++++++++++++++++++ docs/swagger.json | 458 +++++++++++++++++ docs/swagger.yaml | 316 ++++++++++++ go.mod | 2 +- go.sum | 20 +- main.go | 34 +- makefile | 12 +- routes/articles.go | 95 ++++ routes/discordQueue.go | 29 ++ routes/discordwebhooks.go | 115 +++++ routes/root.go | 53 +- routes/root_test.go | 2 + routes/server.go | 108 ++++ routes/settings.go | 44 ++ routes/sources.go | 278 ++++++++++ routes/subscriptions.go | 105 ++++ services/cron/scheduler.go | 94 ++-- services/cron/scheduler_test.go | 15 +- 25 files changed, 2715 insertions(+), 121 deletions(-) create mode 100644 database/migrations/20220619085634_subscriptions.sql create mode 100644 docs/docs.go create mode 100644 docs/swagger.json create mode 100644 docs/swagger.yaml create mode 100644 routes/articles.go create mode 100644 routes/discordQueue.go create mode 100644 routes/discordwebhooks.go create mode 100644 routes/root_test.go create mode 100644 routes/server.go create mode 100644 routes/settings.go create mode 100644 routes/sources.go create mode 100644 routes/subscriptions.go diff --git a/.gitignore b/.gitignore index bc73b1c..e2c67be 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ .env dev.session.sql +.vscode + # Binaries for programs and plugins *.exe *.exe~ diff --git a/database/migrations/20220529082459_seed.sql b/database/migrations/20220529082459_seed.sql index d001ad0..29da683 100644 --- a/database/migrations/20220529082459_seed.sql +++ b/database/migrations/20220529082459_seed.sql @@ -33,7 +33,7 @@ INSERT INTO sources VALUES -- Twitch Entries INSERT INTO sources VALUES -(uuid_generate_v4(), 'twitch', 'Nintendo', 'twitch', 'api', 'a', TRUE, 'https://store.steampowered.com/feeds/news/app/1675200/?cc=US&l=english&snr=1_2108_9__2107', 'rss, steampowered, steam, deck, steam deck'); +(uuid_generate_v4(), 'twitch', 'Nintendo', 'twitch', 'api', 'a', TRUE, 'https://twitch.tv/nintendo', 'twitch, nintendo'); -- +goose StatementEnd diff --git a/database/migrations/20220619085634_subscriptions.sql b/database/migrations/20220619085634_subscriptions.sql new file mode 100644 index 0000000..16bf429 --- /dev/null +++ b/database/migrations/20220619085634_subscriptions.sql @@ -0,0 +1,21 @@ +-- +goose Up +-- +goose StatementBegin +SELECT 'up SQL query'; +Create TABLE Subscriptions ( + ID uuid Primary Key, + DiscordWebHookID uuid Not Null, + SourceID uuid Not Null +); + +ALTER TABLE discordwebhooks drop COLUMN Name; +ALTER TABLE discordwebhooks drop COLUMN Key; + +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +SELECT 'down SQL query'; +Drop Table Subscriptions; +ALTER TABLE discordwebhooks Add COLUMN Name TEXT; +--ALTER TABLE discordwebhooks Add COLUMN Key TEXT; +-- +goose StatementEnd diff --git a/database/models.go b/database/models.go index 58ea663..34c3a97 100644 --- a/database/models.go +++ b/database/models.go @@ -34,8 +34,6 @@ type Discordqueue struct { type Discordwebhook struct { ID uuid.UUID - Name string - Key sql.NullString Url string Server string Channel string @@ -66,3 +64,9 @@ type Source struct { Url string Tags string } + +type Subscription struct { + ID uuid.UUID + Discordwebhookid uuid.UUID + Sourceid uuid.UUID +} diff --git a/database/query.sql.go b/database/query.sql.go index c233cc5..d0a0095 100644 --- a/database/query.sql.go +++ b/database/query.sql.go @@ -75,15 +75,13 @@ func (q *Queries) CreateDiscordQueue(ctx context.Context, arg CreateDiscordQueue const createDiscordWebHook = `-- name: CreateDiscordWebHook :exec Insert Into DiscordWebHooks -(ID, Name, Key, Url, Server, Channel, Enabled) +(ID, Url, Server, Channel, Enabled) Values -($1, $2, $3, $4, $5, $6, $7) +($1, $2, $3, $4, $5) ` type CreateDiscordWebHookParams struct { ID uuid.UUID - Name string - Key sql.NullString Url string Server string Channel string @@ -94,8 +92,6 @@ type CreateDiscordWebHookParams struct { func (q *Queries) CreateDiscordWebHook(ctx context.Context, arg CreateDiscordWebHookParams) error { _, err := q.db.ExecContext(ctx, createDiscordWebHook, arg.ID, - arg.Name, - arg.Key, arg.Url, arg.Server, arg.Channel, @@ -194,6 +190,23 @@ func (q *Queries) CreateSource(ctx context.Context, arg CreateSourceParams) erro return err } +const createSubscription = `-- name: CreateSubscription :exec + +Insert Into subscriptions (ID, DiscordWebHookId, SourceId) Values ($1, $2, $3) +` + +type CreateSubscriptionParams struct { + ID uuid.UUID + Discordwebhookid uuid.UUID + Sourceid uuid.UUID +} + +// Subscriptions +func (q *Queries) CreateSubscription(ctx context.Context, arg CreateSubscriptionParams) error { + _, err := q.db.ExecContext(ctx, createSubscription, arg.ID, arg.Discordwebhookid, arg.Sourceid) + return err +} + const deleteDiscordQueueItem = `-- name: DeleteDiscordQueueItem :exec Delete From DiscordQueue Where ID = $1 ` @@ -239,6 +252,38 @@ func (q *Queries) DeleteSource(ctx context.Context, id uuid.UUID) error { return err } +const deleteSubscription = `-- name: DeleteSubscription :exec +Delete From subscriptions Where discordwebhookid = $1 and sourceid = $2 +` + +type DeleteSubscriptionParams struct { + Discordwebhookid uuid.UUID + Sourceid uuid.UUID +} + +func (q *Queries) DeleteSubscription(ctx context.Context, arg DeleteSubscriptionParams) error { + _, err := q.db.ExecContext(ctx, deleteSubscription, arg.Discordwebhookid, arg.Sourceid) + return err +} + +const disableSource = `-- name: DisableSource :exec +Update Sources Set Enabled = FALSE where ID = $1 +` + +func (q *Queries) DisableSource(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, disableSource, id) + return err +} + +const enableSource = `-- name: EnableSource :exec +Update Sources Set Enabled = TRUE where ID = $1 +` + +func (q *Queries) EnableSource(ctx context.Context, id uuid.UUID) error { + _, err := q.db.ExecContext(ctx, enableSource, id) + return err +} + const getArticleByID = `-- name: GetArticleByID :one Select id, sourceid, tags, title, url, pubdate, video, videoheight, videowidth, thumbnail, description, authorname, authorimage from Articles WHERE ID = $1 LIMIT 1 @@ -292,6 +337,191 @@ func (q *Queries) GetArticleByUrl(ctx context.Context, url string) (Article, err return i, err } +const getArticlesBySource = `-- name: GetArticlesBySource :many +select articles.id, sourceid, articles.tags, title, articles.url, pubdate, video, videoheight, videowidth, thumbnail, description, authorname, authorimage, sources.id, site, name, source, type, value, enabled, sources.url, sources.tags from articles +INNER join sources on articles.sourceid=Sources.ID +where site = $1 +` + +type GetArticlesBySourceRow 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 + ID_2 uuid.UUID + Site string + Name string + Source string + Type string + Value sql.NullString + Enabled bool + Url_2 string + Tags_2 string +} + +func (q *Queries) GetArticlesBySource(ctx context.Context, site string) ([]GetArticlesBySourceRow, error) { + rows, err := q.db.QueryContext(ctx, getArticlesBySource, site) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetArticlesBySourceRow + for rows.Next() { + var i GetArticlesBySourceRow + if err := rows.Scan( + &i.ID, + &i.Sourceid, + &i.Tags, + &i.Title, + &i.Url, + &i.Pubdate, + &i.Video, + &i.Videoheight, + &i.Videowidth, + &i.Thumbnail, + &i.Description, + &i.Authorname, + &i.Authorimage, + &i.ID_2, + &i.Site, + &i.Name, + &i.Source, + &i.Type, + &i.Value, + &i.Enabled, + &i.Url_2, + &i.Tags_2, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getArticlesBySourceId = `-- name: GetArticlesBySourceId :many +Select id, sourceid, tags, title, url, pubdate, video, videoheight, videowidth, thumbnail, description, authorname, authorimage From articles +Where sourceid = $1 Limit 50 +` + +func (q *Queries) GetArticlesBySourceId(ctx context.Context, sourceid uuid.UUID) ([]Article, error) { + rows, err := q.db.QueryContext(ctx, getArticlesBySourceId, sourceid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Article + for rows.Next() { + var i Article + if err := rows.Scan( + &i.ID, + &i.Sourceid, + &i.Tags, + &i.Title, + &i.Url, + &i.Pubdate, + &i.Video, + &i.Videoheight, + &i.Videowidth, + &i.Thumbnail, + &i.Description, + &i.Authorname, + &i.Authorimage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getArticlesBySourceName = `-- 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 +` + +type GetArticlesBySourceNameRow 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 + Source sql.NullString + Name sql.NullString +} + +func (q *Queries) GetArticlesBySourceName(ctx context.Context, name string) ([]GetArticlesBySourceNameRow, error) { + rows, err := q.db.QueryContext(ctx, getArticlesBySourceName, name) + if err != nil { + return nil, err + } + defer rows.Close() + var items []GetArticlesBySourceNameRow + for rows.Next() { + var i GetArticlesBySourceNameRow + if err := rows.Scan( + &i.ID, + &i.Sourceid, + &i.Tags, + &i.Title, + &i.Url, + &i.Pubdate, + &i.Video, + &i.Videoheight, + &i.Videowidth, + &i.Thumbnail, + &i.Description, + &i.Authorname, + &i.Authorimage, + &i.Source, + &i.Name, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const getDiscordQueueByID = `-- name: GetDiscordQueueByID :one Select id, articleid from DiscordQueue Where ID = $1 LIMIT 1 @@ -332,7 +562,7 @@ func (q *Queries) GetDiscordQueueItems(ctx context.Context, limit int32) ([]Disc } const getDiscordWebHooksByID = `-- name: GetDiscordWebHooksByID :one -Select id, name, key, url, server, channel, enabled from DiscordWebHooks +Select id, url, server, channel, enabled from DiscordWebHooks Where ID = $1 LIMIT 1 ` @@ -341,8 +571,6 @@ func (q *Queries) GetDiscordWebHooksByID(ctx context.Context, id uuid.UUID) (Dis var i Discordwebhook err := row.Scan( &i.ID, - &i.Name, - &i.Key, &i.Url, &i.Server, &i.Channel, @@ -447,8 +675,103 @@ func (q *Queries) GetSourceByID(ctx context.Context, id uuid.UUID) (Source, erro return i, err } +const getSubscriptionsByDiscordWebHookId = `-- name: GetSubscriptionsByDiscordWebHookId :many +Select id, discordwebhookid, sourceid from subscriptions Where discordwebhookid = $1 +` + +func (q *Queries) GetSubscriptionsByDiscordWebHookId(ctx context.Context, discordwebhookid uuid.UUID) ([]Subscription, error) { + rows, err := q.db.QueryContext(ctx, getSubscriptionsByDiscordWebHookId, discordwebhookid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Subscription + for rows.Next() { + var i Subscription + if err := rows.Scan(&i.ID, &i.Discordwebhookid, &i.Sourceid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const getSubscriptionsBySourceID = `-- name: GetSubscriptionsBySourceID :many +Select id, discordwebhookid, sourceid From subscriptions Where sourceid = $1 +` + +func (q *Queries) GetSubscriptionsBySourceID(ctx context.Context, sourceid uuid.UUID) ([]Subscription, error) { + rows, err := q.db.QueryContext(ctx, getSubscriptionsBySourceID, sourceid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Subscription + for rows.Next() { + var i Subscription + if err := rows.Scan(&i.ID, &i.Discordwebhookid, &i.Sourceid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listArticles = `-- name: ListArticles :many +Select id, sourceid, tags, title, url, pubdate, video, videoheight, videowidth, thumbnail, description, authorname, authorimage From articles Limit $1 +` + +func (q *Queries) ListArticles(ctx context.Context, limit int32) ([]Article, error) { + rows, err := q.db.QueryContext(ctx, listArticles, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Article + for rows.Next() { + var i Article + if err := rows.Scan( + &i.ID, + &i.Sourceid, + &i.Tags, + &i.Title, + &i.Url, + &i.Pubdate, + &i.Video, + &i.Videoheight, + &i.Videowidth, + &i.Thumbnail, + &i.Description, + &i.Authorname, + &i.Authorimage, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listDiscordWebHooksByServer = `-- name: ListDiscordWebHooksByServer :many -Select id, name, key, url, server, channel, enabled From DiscordWebHooks +Select id, url, server, channel, enabled From DiscordWebHooks Where Server = $1 ` @@ -463,8 +786,6 @@ func (q *Queries) ListDiscordWebHooksByServer(ctx context.Context, server string var i Discordwebhook if err := rows.Scan( &i.ID, - &i.Name, - &i.Key, &i.Url, &i.Server, &i.Channel, @@ -483,6 +804,76 @@ func (q *Queries) ListDiscordWebHooksByServer(ctx context.Context, server string return items, nil } +const listDiscordWebhooks = `-- name: ListDiscordWebhooks :many +Select id, url, server, channel, enabled From discordwebhooks LIMIT $1 +` + +func (q *Queries) ListDiscordWebhooks(ctx context.Context, limit int32) ([]Discordwebhook, error) { + rows, err := q.db.QueryContext(ctx, listDiscordWebhooks, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Discordwebhook + for rows.Next() { + var i Discordwebhook + if err := rows.Scan( + &i.ID, + &i.Url, + &i.Server, + &i.Channel, + &i.Enabled, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const listSources = `-- name: ListSources :many +Select id, site, name, source, type, value, enabled, url, tags From Sources Limit $1 +` + +func (q *Queries) ListSources(ctx context.Context, limit int32) ([]Source, error) { + rows, err := q.db.QueryContext(ctx, listSources, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Source + for rows.Next() { + var i Source + if err := rows.Scan( + &i.ID, + &i.Site, + &i.Name, + &i.Source, + &i.Type, + &i.Value, + &i.Enabled, + &i.Url, + &i.Tags, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const listSourcesBySource = `-- name: ListSourcesBySource :many Select id, site, name, source, type, value, enabled, url, tags From Sources where Source = $1 ` @@ -519,3 +910,62 @@ func (q *Queries) ListSourcesBySource(ctx context.Context, source string) ([]Sou } return items, nil } + +const listSubscriptions = `-- name: ListSubscriptions :many +Select id, discordwebhookid, sourceid From subscriptions Limit $1 +` + +func (q *Queries) ListSubscriptions(ctx context.Context, limit int32) ([]Subscription, error) { + rows, err := q.db.QueryContext(ctx, listSubscriptions, limit) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Subscription + for rows.Next() { + var i Subscription + if err := rows.Scan(&i.ID, &i.Discordwebhookid, &i.Sourceid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const querySubscriptions = `-- name: QuerySubscriptions :many +Select id, discordwebhookid, sourceid From subscriptions Where discordwebhookid = $1 and sourceid = $2 +` + +type QuerySubscriptionsParams struct { + Discordwebhookid uuid.UUID + Sourceid uuid.UUID +} + +func (q *Queries) QuerySubscriptions(ctx context.Context, arg QuerySubscriptionsParams) ([]Subscription, error) { + rows, err := q.db.QueryContext(ctx, querySubscriptions, arg.Discordwebhookid, arg.Sourceid) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Subscription + for rows.Next() { + var i Subscription + if err := rows.Scan(&i.ID, &i.Discordwebhookid, &i.Sourceid); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/database/schema/query.sql b/database/schema/query.sql index d475ab7..c17e231 100644 --- a/database/schema/query.sql +++ b/database/schema/query.sql @@ -7,6 +7,26 @@ WHERE ID = $1 LIMIT 1; Select * from Articles Where Url = $1 LIMIT 1; +-- name: ListArticles :many +Select * From articles Limit $1; + +-- name: GetArticlesBySource :many +select * from articles +INNER join sources on articles.sourceid=Sources.ID +where site = $1; + +-- name: GetArticlesBySourceId :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: CreateArticle :exec INSERT INTO Articles (ID, SourceId, Tags, Title, Url, PubDate, Video, VideoHeight, VideoWidth, Thumbnail, Description, AuthorName, AuthorImage) @@ -31,13 +51,12 @@ Delete From DiscordQueue Where ID = $1; -- name: GetDiscordQueueItems :many Select * from DiscordQueue LIMIT $1; - /* DiscordWebHooks */ -- name: CreateDiscordWebHook :exec Insert Into DiscordWebHooks -(ID, Name, Key, Url, Server, Channel, Enabled) +(ID, Url, Server, Channel, Enabled) Values -($1, $2, $3, $4, $5, $6, $7); +($1, $2, $3, $4, $5); -- name: GetDiscordWebHooksByID :one Select * from DiscordWebHooks @@ -47,6 +66,9 @@ Where ID = $1 LIMIT 1; Select * From DiscordWebHooks Where Server = $1; +-- name: ListDiscordWebhooks :many +Select * From discordwebhooks LIMIT $1; + -- name: DeleteDiscordWebHooks :exec Delete From discordwebhooks Where ID = $1; @@ -105,8 +127,38 @@ Values -- name: GetSourceByID :one Select * From Sources where ID = $1 Limit 1; +-- name: ListSources :many +Select * From Sources Limit $1; + -- name: ListSourcesBySource :many Select * From Sources where Source = $1; -- name: DeleteSource :exec DELETE From sources 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: QuerySubscriptions :many +Select * From subscriptions Where discordwebhookid = $1 and sourceid = $2; + +-- 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 discordwebhookid = $1 and sourceid = $2; \ No newline at end of file diff --git a/database/schema/schema.sql b/database/schema/schema.sql index fe03468..e65a0f4 100644 --- a/database/schema/schema.sql +++ b/database/schema/schema.sql @@ -21,8 +21,6 @@ CREATE Table DiscordQueue ( CREATE Table DiscordWebHooks ( ID uuid PRIMARY KEY, - Name TEXT NOT NULL, -- Defines webhook purpose - Key TEXT, 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 @@ -54,3 +52,9 @@ Create Table Sources ( Tags TEXT NOT NULL ); +/* 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 +); diff --git a/docs/docs.go b/docs/docs.go new file mode 100644 index 0000000..44f8639 --- /dev/null +++ b/docs/docs.go @@ -0,0 +1,483 @@ +// Package docs GENERATED BY SWAG; DO NOT EDIT +// This file was generated by swaggo/swag +package docs + +import "github.com/swaggo/swag" + +const docTemplate = `{ + "schemes": {{ marshal .Schemes }}, + "swagger": "2.0", + "info": { + "description": "{{escape .Description}}", + "title": "{{.Title}}", + "contact": {}, + "version": "{{.Version}}" + }, + "host": "{{.Host}}", + "basePath": "{{.BasePath}}", + "paths": { + "/articles": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "articles" + ], + "summary": "Lists the top 50 records", + "responses": {} + } + }, + "/articles/by/sourceid/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "articles" + ], + "summary": "Finds the articles based on the SourceID provided. Returns the top 50.", + "parameters": [ + { + "type": "string", + "description": "Source ID UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/articles/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "articles" + ], + "summary": "Returns an article based on defined ID.", + "parameters": [ + { + "type": "string", + "description": "uuid", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "source" + ], + "summary": "Lists the top 50 records", + "responses": {} + } + }, + "/config/sources/new/reddit": { + "post": { + "tags": [ + "config", + "source", + "reddit" + ], + "summary": "Creates a new reddit source to monitor.", + "parameters": [ + { + "type": "string", + "description": "name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/new/twitch": { + "post": { + "tags": [ + "config", + "source", + "twitch" + ], + "summary": "Creates a new twitch source to monitor.", + "parameters": [ + { + "type": "string", + "description": "name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "tags", + "name": "tags", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/new/youtube": { + "post": { + "tags": [ + "config", + "source", + "youtube" + ], + "summary": "Creates a new youtube source to monitor.", + "parameters": [ + { + "type": "string", + "description": "name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "tags", + "name": "tags", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "source" + ], + "summary": "Returns a single entity by ID", + "parameters": [ + { + "type": "string", + "description": "uuid", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + }, + "delete": { + "tags": [ + "config", + "source" + ], + "summary": "Deletes a record by ID.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/{id}/disable": { + "post": { + "tags": [ + "config", + "source" + ], + "summary": "Disables a source from processing.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/{id}/enable": { + "post": { + "tags": [ + "config", + "source" + ], + "summary": "Enables a source to continue processing.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/discord/queue": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "debug", + "Discord", + "Queue" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "responses": {} + } + }, + "/discord/webhooks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Discord", + "Webhooks" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "responses": {} + } + }, + "/discord/webhooks/byId": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Discord", + "Webhooks" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/discord/webhooks/new": { + "post": { + "tags": [ + "config", + "Discord", + "Webhooks" + ], + "summary": "Creates a new record for a discord web hook to post data to.", + "parameters": [ + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Server name", + "name": "server", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Channel name.", + "name": "channel", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/hello/{who}": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "debug" + ], + "summary": "Responds back with \"Hello x\" depending on param passed in.", + "parameters": [ + { + "type": "string", + "description": "Who", + "name": "who", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/helloworld": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "debug" + ], + "summary": "Responds back with \"Hello world!\"", + "responses": {} + } + }, + "/ping": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "debug" + ], + "summary": "Sends back \"pong\". Good to test with.", + "responses": {} + } + }, + "/settings/{key}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Returns a object based on the Key that was given/", + "parameters": [ + { + "type": "string", + "description": "Settings Key value", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/subscriptions": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Subscriptions" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "responses": {} + } + }, + "/subscriptions/byDiscordId": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Subscriptions" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/subscriptions/bySourceId": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Subscriptions" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": {} + } + } + } +}` + +// SwaggerInfo holds exported Swagger Info so clients can modify it +var SwaggerInfo = &swag.Spec{ + Version: "0.1", + Host: "", + BasePath: "/api", + Schemes: []string{}, + Title: "NewsBot collector", + Description: "", + InfoInstanceName: "swagger", + SwaggerTemplate: docTemplate, +} + +func init() { + swag.Register(SwaggerInfo.InstanceName(), SwaggerInfo) +} diff --git a/docs/swagger.json b/docs/swagger.json new file mode 100644 index 0000000..3d9aa36 --- /dev/null +++ b/docs/swagger.json @@ -0,0 +1,458 @@ +{ + "swagger": "2.0", + "info": { + "title": "NewsBot collector", + "contact": {}, + "version": "0.1" + }, + "basePath": "/api", + "paths": { + "/articles": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "articles" + ], + "summary": "Lists the top 50 records", + "responses": {} + } + }, + "/articles/by/sourceid/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "articles" + ], + "summary": "Finds the articles based on the SourceID provided. Returns the top 50.", + "parameters": [ + { + "type": "string", + "description": "Source ID UUID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/articles/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "articles" + ], + "summary": "Returns an article based on defined ID.", + "parameters": [ + { + "type": "string", + "description": "uuid", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "source" + ], + "summary": "Lists the top 50 records", + "responses": {} + } + }, + "/config/sources/new/reddit": { + "post": { + "tags": [ + "config", + "source", + "reddit" + ], + "summary": "Creates a new reddit source to monitor.", + "parameters": [ + { + "type": "string", + "description": "name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/new/twitch": { + "post": { + "tags": [ + "config", + "source", + "twitch" + ], + "summary": "Creates a new twitch source to monitor.", + "parameters": [ + { + "type": "string", + "description": "name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "tags", + "name": "tags", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/new/youtube": { + "post": { + "tags": [ + "config", + "source", + "youtube" + ], + "summary": "Creates a new youtube source to monitor.", + "parameters": [ + { + "type": "string", + "description": "name", + "name": "name", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "tags", + "name": "tags", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/{id}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "source" + ], + "summary": "Returns a single entity by ID", + "parameters": [ + { + "type": "string", + "description": "uuid", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + }, + "delete": { + "tags": [ + "config", + "source" + ], + "summary": "Deletes a record by ID.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/{id}/disable": { + "post": { + "tags": [ + "config", + "source" + ], + "summary": "Disables a source from processing.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/config/sources/{id}/enable": { + "post": { + "tags": [ + "config", + "source" + ], + "summary": "Enables a source to continue processing.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/discord/queue": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "debug", + "Discord", + "Queue" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "responses": {} + } + }, + "/discord/webhooks": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Discord", + "Webhooks" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "responses": {} + } + }, + "/discord/webhooks/byId": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Discord", + "Webhooks" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/discord/webhooks/new": { + "post": { + "tags": [ + "config", + "Discord", + "Webhooks" + ], + "summary": "Creates a new record for a discord web hook to post data to.", + "parameters": [ + { + "type": "string", + "description": "url", + "name": "url", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Server name", + "name": "server", + "in": "query", + "required": true + }, + { + "type": "string", + "description": "Channel name.", + "name": "channel", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/hello/{who}": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "debug" + ], + "summary": "Responds back with \"Hello x\" depending on param passed in.", + "parameters": [ + { + "type": "string", + "description": "Who", + "name": "who", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/helloworld": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "debug" + ], + "summary": "Responds back with \"Hello world!\"", + "responses": {} + } + }, + "/ping": { + "get": { + "produces": [ + "text/plain" + ], + "tags": [ + "debug" + ], + "summary": "Sends back \"pong\". Good to test with.", + "responses": {} + } + }, + "/settings/{key}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "settings" + ], + "summary": "Returns a object based on the Key that was given/", + "parameters": [ + { + "type": "string", + "description": "Settings Key value", + "name": "key", + "in": "path", + "required": true + } + ], + "responses": {} + } + }, + "/subscriptions": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Subscriptions" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "responses": {} + } + }, + "/subscriptions/byDiscordId": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Subscriptions" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": {} + } + }, + "/subscriptions/bySourceId": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "config", + "Subscriptions" + ], + "summary": "Returns the top 100 entries from the queue to be processed.", + "parameters": [ + { + "type": "string", + "description": "id", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": {} + } + } + } +} \ No newline at end of file diff --git a/docs/swagger.yaml b/docs/swagger.yaml new file mode 100644 index 0000000..40a85bb --- /dev/null +++ b/docs/swagger.yaml @@ -0,0 +1,316 @@ +basePath: /api +info: + contact: {} + title: NewsBot collector + version: "0.1" +paths: + /articles: + get: + produces: + - application/json + responses: {} + summary: Lists the top 50 records + tags: + - articles + /articles/{id}: + get: + parameters: + - description: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: {} + summary: Returns an article based on defined ID. + tags: + - articles + /articles/by/sourceid/{id}: + get: + parameters: + - description: Source ID UUID + in: path + name: id + required: true + type: string + produces: + - application/json + responses: {} + summary: Finds the articles based on the SourceID provided. Returns the top + 50. + tags: + - articles + /config/sources: + get: + produces: + - application/json + responses: {} + summary: Lists the top 50 records + tags: + - config + - source + /config/sources/{id}: + delete: + parameters: + - description: id + in: path + name: id + required: true + type: string + responses: {} + summary: Deletes a record by ID. + tags: + - config + - source + get: + parameters: + - description: uuid + in: path + name: id + required: true + type: string + produces: + - application/json + responses: {} + summary: Returns a single entity by ID + tags: + - config + - source + /config/sources/{id}/disable: + post: + parameters: + - description: id + in: path + name: id + required: true + type: string + responses: {} + summary: Disables a source from processing. + tags: + - config + - source + /config/sources/{id}/enable: + post: + parameters: + - description: id + in: path + name: id + required: true + type: string + responses: {} + summary: Enables a source to continue processing. + tags: + - config + - source + /config/sources/new/reddit: + post: + parameters: + - description: name + in: query + name: name + required: true + type: string + - description: url + in: query + name: url + required: true + type: string + responses: {} + summary: Creates a new reddit source to monitor. + tags: + - config + - source + - reddit + /config/sources/new/twitch: + post: + parameters: + - description: name + in: query + name: name + required: true + type: string + - description: url + in: query + name: url + required: true + type: string + - description: tags + in: query + name: tags + required: true + type: string + responses: {} + summary: Creates a new twitch source to monitor. + tags: + - config + - source + - twitch + /config/sources/new/youtube: + post: + parameters: + - description: name + in: query + name: name + required: true + type: string + - description: url + in: query + name: url + required: true + type: string + - description: tags + in: query + name: tags + required: true + type: string + responses: {} + summary: Creates a new youtube source to monitor. + tags: + - config + - source + - youtube + /discord/queue: + get: + produces: + - application/json + responses: {} + summary: Returns the top 100 entries from the queue to be processed. + tags: + - debug + - Discord + - Queue + /discord/webhooks: + get: + produces: + - application/json + responses: {} + summary: Returns the top 100 entries from the queue to be processed. + tags: + - config + - Discord + - Webhooks + /discord/webhooks/byId: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: {} + summary: Returns the top 100 entries from the queue to be processed. + tags: + - config + - Discord + - Webhooks + /discord/webhooks/new: + post: + parameters: + - description: url + in: query + name: url + required: true + type: string + - description: Server name + in: query + name: server + required: true + type: string + - description: Channel name. + in: query + name: channel + required: true + type: string + responses: {} + summary: Creates a new record for a discord web hook to post data to. + tags: + - config + - Discord + - Webhooks + /hello/{who}: + get: + parameters: + - description: Who + in: path + name: who + required: true + type: string + produces: + - text/plain + responses: {} + summary: Responds back with "Hello x" depending on param passed in. + tags: + - debug + /helloworld: + get: + produces: + - text/plain + responses: {} + summary: Responds back with "Hello world!" + tags: + - debug + /ping: + get: + produces: + - text/plain + responses: {} + summary: Sends back "pong". Good to test with. + tags: + - debug + /settings/{key}: + get: + parameters: + - description: Settings Key value + in: path + name: key + required: true + type: string + produces: + - application/json + responses: {} + summary: Returns a object based on the Key that was given/ + tags: + - settings + /subscriptions: + get: + produces: + - application/json + responses: {} + summary: Returns the top 100 entries from the queue to be processed. + tags: + - config + - Subscriptions + /subscriptions/byDiscordId: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: {} + summary: Returns the top 100 entries from the queue to be processed. + tags: + - config + - Subscriptions + /subscriptions/bySourceId: + get: + parameters: + - description: id + in: query + name: id + required: true + type: string + produces: + - application/json + responses: {} + summary: Returns the top 100 entries from the queue to be processed. + tags: + - config + - Subscriptions +swagger: "2.0" diff --git a/go.mod b/go.mod index 0ff1df3..6e44a8e 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/go-rod/rod v0.107.1 github.com/google/uuid v1.3.0 github.com/joho/godotenv v1.4.0 - github.com/lib/pq v1.10.6 github.com/mmcdole/gofeed v1.1.3 github.com/nicklaw5/helix/v2 v2.4.0 github.com/robfig/cron/v3 v3.0.1 @@ -26,6 +25,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.4.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect + github.com/lib/pq v1.10.6 github.com/mailru/easyjson v0.7.7 // indirect github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect diff --git a/go.sum b/go.sum index a8a418c..99d5be8 100644 --- a/go.sum +++ b/go.sum @@ -25,8 +25,6 @@ github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.21.1 h1:wm0rhTb5z7qpJRHBdPOMuY4QjVUMbF6/kwoYeRAOrKU= github.com/go-openapi/swag v0.21.1/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= -github.com/go-rod/rod v0.105.1 h1:r0bNmO9siOe13lG6Vbkaak11u48rYmWGl/Hk4MJdOiE= -github.com/go-rod/rod v0.105.1/go.mod h1:Wrnn6HokFHskwaIVke3ML1y/NBVp7XPIeB8eDzR9vuw= 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/golang-jwt/jwt/v4 v4.4.1 h1:pC5DB52sCeK48Wlb9oPcdhnjkz1TKt1D/P7WKJ0kUcQ= @@ -38,7 +36,6 @@ github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr68= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= @@ -56,15 +53,12 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mmcdole/gofeed v1.1.3 h1:pdrvMb18jMSLidGp8j0pLvc9IGziX4vbmvVqmLH6z8o= github.com/mmcdole/gofeed v1.1.3/go.mod h1:QQO3maftbOu+hiVOGOZDRLymqGQCos4zxbA4j89gMrE= -github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf h1:sWGE2v+hO0Nd4yFU/S/mDBM5plIU8v/Qhfz41hkDIAI= github.com/mmcdole/goxpp v0.0.0-20181012175147-0068e33feabf/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354 h1:Z6i7ND25ixRtXFBylIUggqpvLMV1I15yprcqMVB7WZA= github.com/mmcdole/goxpp v0.0.0-20200921145534-2f3784f67354/go.mod h1:pasqhqstspkosTneA62Nc+2p9SOBBYAPbnmRRWPQ0V8= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= @@ -82,12 +76,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= -github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2 h1:+iNTcqQJy0OZ5jk6a5NLib47eqXK8uYcPX+O4+cBpEM= -github.com/swaggo/files v0.0.0-20210815190702-a29dd2bc99b2/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe h1:K8pHPVoTgxFJt1lXuIzzOX7zZhZFldJQK/CgKx9BFIc= github.com/swaggo/files v0.0.0-20220610200504-28940afbdbfe/go.mod h1:lKJPbtWzJ9JhsTN1k1gZgleJWY/cqq0psdoMmaThG3w= -github.com/swaggo/http-swagger v1.2.8 h1:TVjxLU7qoqofJ9qynJazmpTGs/p4Kx9FTp7YYwOkJb0= -github.com/swaggo/http-swagger v1.2.8/go.mod h1:FrQwV7rx+A5t11PIX8d+tFJa2GKx11RdAXQptllPQHg= github.com/swaggo/http-swagger v1.3.0 h1:1+6M4qRorIbdyTWTsGrwnb0r9jGK5dcWN82O6oY/yHQ= github.com/swaggo/http-swagger v1.3.0/go.mod h1:9glekdg40lwclrrKNRGgj/IMDxpNPZ3kzab4oPcF8EM= github.com/swaggo/swag v1.8.2 h1:D4aBiVS2a65zhyk3WFqOUz7Rz0sOaUcgeErcid5uGL4= @@ -95,20 +85,17 @@ github.com/swaggo/swag v1.8.2/go.mod h1:jMLeXOOmYyjk8PvHTsXBdrubsNd9gUJTTCzL5iBn github.com/urfave/cli v1.22.3/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/ysmood/goob v0.4.0 h1:HsxXhyLBeGzWXnqVKtmT9qM7EuVs/XOgkX7T6r1o1AQ= github.com/ysmood/goob v0.4.0/go.mod h1:u6yx7ZhS4Exf2MwciFr6nIM8knHQIE22lFpWHnfql18= -github.com/ysmood/got v0.23.2 h1:U2U0vyQ/gDaawkKJZK/hyza8UUXbWCurbmazK7AcAfY= -github.com/ysmood/got v0.23.2/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= github.com/ysmood/got v0.29.5 h1:+wMnm8UjoyYFMfeAsr57a1bahWTkloysc0Hxsu2gmnM= github.com/ysmood/got v0.29.5/go.mod h1:pE1l4LOwOBhQg6A/8IAatkGp7uZjnalzrZolnlhhMgY= github.com/ysmood/gotrace v0.6.0 h1:SyI1d4jclswLhg7SWTL6os3L1WOKeNn/ZtzVQF8QmdY= github.com/ysmood/gotrace v0.6.0/go.mod h1:TzhIG7nHDry5//eYZDYcTzuJLYQIkykJzCRIo4/dzQM= -github.com/ysmood/gson v0.7.1 h1:zKL2MTGtynxdBdlZjyGsvEOZ7dkxaY5TH6QhAbTgz0Q= github.com/ysmood/gson v0.7.1/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/gson v0.7.2 h1:1iWUvpi5DPvd2j59W7ifRPR9DiAZ3Ga+fmMl1mJrRbM= github.com/ysmood/gson v0.7.2/go.mod h1:3Kzs5zDl21g5F/BlLTNcuAGAYLKt2lV5G8D1zF3RNmg= github.com/ysmood/leakless v0.7.0 h1:XCGdaPExyoreoQd+H5qgxM3ReNbSPFsEXpSKwbXbwQw= github.com/ysmood/leakless v0.7.0/go.mod h1:R8iAXPRaG97QJwqxs74RdwzcRHT1SWCGTNqY8q0JvMQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 h1:6zppjxzCulZykYSLyVDYbneBfbaBIQPYMevg0bEwv2s= golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -119,8 +106,6 @@ golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68 h1:z8Hj/bl9cOV2grsOpEaQFUaly0JWN3i97mo3jXKJNp0= -golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098 h1:PgOr27OhUx2IRqGJ2RxAWI4dJQ7bi9cSrB82uzFzfUA= golang.org/x/sys v0.0.0-20220614162138-6c1b26c55098/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -130,11 +115,8 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= -golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= golang.org/x/tools v0.1.11 h1:loJ25fNOEhSXfHrpoGj91eCUThwdNX6u24rO1xnNteY= golang.org/x/tools v0.1.11/go.mod h1:SgwaegtQh8clINPpECJMqnxLv9I09HLqnW3RMqW0CA4= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= diff --git a/main.go b/main.go index f963cbd..0286896 100644 --- a/main.go +++ b/main.go @@ -2,31 +2,29 @@ package main import ( "context" - "log" + "fmt" "net/http" - - "github.com/go-chi/chi/v5" - "github.com/go-chi/chi/v5/middleware" - + + _ "github.com/jtom38/newsbot/collector/docs" "github.com/jtom38/newsbot/collector/routes" "github.com/jtom38/newsbot/collector/services/cron" ) + + +// @title NewsBot collector +// @version 0.1 +// @BasePath /api func main() { ctx := context.Background() + c := cron.New(ctx) + c.Start() - cron.EnableScheduler(ctx) + server := routes.NewServer(ctx) - app := chi.NewRouter() - app.Use(middleware.Logger) - app.Use(middleware.Recoverer) - - //app.Mount("/swagger", httpSwagger.WrapHandler) - app.Mount("/api", routes.RootRoutes()) - - log.Println("API is online and waiting for requests.") - log.Println("API: http://localhost:8081/api") - //log.Println("Swagger: http://localhost:8080/swagger/index.html") - err := http.ListenAndServe(":8081", app) - if err != nil { log.Fatalln(err) } + fmt.Println("API is online and waiting for requests.") + fmt.Println("API: http://localhost:8081/api") + fmt.Println("Swagger: http://localhost:8081/swagger/index.html") + err := http.ListenAndServe(":8081", server.Router) + if err != nil { panic(err) } } \ No newline at end of file diff --git a/makefile b/makefile index 07c1eb7..7fa24b5 100644 --- a/makefile +++ b/makefile @@ -3,6 +3,9 @@ help: ## Shows this help command @egrep -h '\s##\s' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' build: ## builds the application with the current go runtime + sqlc generate + ~/go/bin/swag f + ~/go/bin/swag i go build . docker-build: ## Generates the docker image @@ -13,4 +16,11 @@ migrate-dev: ## Apply sql migrations to dev db goose -dir "./database/migrations" postgres "user=postgres password=postgres dbname=postgres sslmode=disable" up migrate-dev-down: ## revert sql migrations to dev db - goose -dir "./database/migrations" postgres "user=postgres password=postgres dbname=postgres sslmode=disable" down \ No newline at end of file + goose -dir "./database/migrations" postgres "user=postgres password=postgres dbname=postgres sslmode=disable" down + +swag: ## Generates the swagger documentation with the swag tool + ~/go/bin/swag f + ~/go/bin/swag i + +gensql: ## Generates SQL code with sqlc + sqlc generate \ No newline at end of file diff --git a/routes/articles.go b/routes/articles.go new file mode 100644 index 0000000..504d658 --- /dev/null +++ b/routes/articles.go @@ -0,0 +1,95 @@ +package routes + +import ( + "encoding/json" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" +) + +// ListArticles +// @Summary Lists the top 50 records +// @Produce application/json +// @Tags articles +// @Router /articles [get] +func (s *Server) listArticles(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + res, err := s.Db.ListArticles(*s.ctx, 50) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + bres, err := json.Marshal(res) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + w.Write(bres) +} + +// GetArticleById +// @Summary Returns an article based on defined ID. +// @Param id path string true "uuid" +// @Produce application/json +// @Tags articles +// @Router /articles/{id} [get] +func (s *Server) getArticleById(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + id := chi.URLParam(r, "ID") + uuid, err := uuid.Parse(id) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + res, err := s.Db.GetArticleByID(*s.ctx, uuid) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + bres, err := json.Marshal(res) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + w.Write(bres) +} + +// TODO add page support +// GetArticlesBySourceID +// @Summary Finds the articles based on the SourceID provided. Returns the top 50. +// @Param id path string true "Source ID UUID" +// @Produce application/json +// @Tags articles +// @Router /articles/by/sourceid/{id} [get] +func (s *Server) GetArticlesBySourceId(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + id := chi.URLParam(r, "ID") + uuid, err := uuid.Parse(id) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + res, err := s.Db.GetArticlesBySourceId(*s.ctx, uuid) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + bres, err := json.Marshal(res) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + w.Write(bres) +} \ No newline at end of file diff --git a/routes/discordQueue.go b/routes/discordQueue.go new file mode 100644 index 0000000..efbdb71 --- /dev/null +++ b/routes/discordQueue.go @@ -0,0 +1,29 @@ +package routes + +import ( + "encoding/json" + "net/http" +) + +// GetDiscordQueue +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Tags debug, Discord, Queue +// @Router /discord/queue [get] +func (s *Server) GetDiscordQueue(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + res, err := s.Db.GetDiscordQueueItems(*s.ctx, 100) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + bres, err := json.Marshal(res) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + w.Write(bres) +} \ No newline at end of file diff --git a/routes/discordwebhooks.go b/routes/discordwebhooks.go new file mode 100644 index 0000000..81d3784 --- /dev/null +++ b/routes/discordwebhooks.go @@ -0,0 +1,115 @@ +package routes + +import ( + "encoding/json" + "log" + "net/http" + "strings" + + "github.com/google/uuid" + "github.com/jtom38/newsbot/collector/database" +) + +// GetDiscordWebHooks +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Tags config, Discord, Webhooks +// @Router /discord/webhooks [get] +func (s *Server) GetDiscordWebHooks(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + res, err := s.Db.ListDiscordWebhooks(*s.ctx, 100) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + bres, err := json.Marshal(res) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + w.Write(bres) +} + +// GetDiscorWebHooksById +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Param id query string true "id" +// @Tags config, Discord, Webhooks +// @Router /discord/webhooks/byId [get] +func (s *Server) GetDiscordWebHooksById(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + query := r.URL.Query() + _id := query["id"][0] + if _id == "" { + http.Error(w, "id is missing", http.StatusBadRequest) + return + } + + uuid, err := uuid.Parse(_id) + if err != nil { + http.Error(w, "unable to parse id value", http.StatusBadRequest) + return + } + + res, err := s.Db.GetDiscordWebHooksByID(*s.ctx, uuid) + if err != nil { + http.Error(w, "no record found", http.StatusBadRequest) + return + } + + bres, err := json.Marshal(res) + if err != nil { + http.Error(w, "unable to convert to json", http.StatusBadRequest) + panic(err) + } + + w.Write(bres) +} + +// NewDiscordWebHook +// @Summary Creates a new record for a discord web hook to post data to. +// @Param url query string true "url" +// @Param server query string true "Server name" +// @Param channel query string true "Channel name." +// @Tags config, Discord, Webhooks +// @Router /discord/webhooks/new [post] +func (s *Server) NewDiscordWebHook(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + _url := query["url"][0] + _server := query["server"][0] + _channel := query["channel"][0] + + if _url == "" { + http.Error(w, "url is missing a value", http.StatusBadRequest) + return + } + if !strings.Contains(_url, "discord.com/api/webhooks") { + http.Error(w, "invalid url", http.StatusBadRequest) + return + } + if _server == ""{ + http.Error(w, "server is missing", http.StatusBadRequest) + } + if _channel == "" { + http.Error(w, "channel is missing", http.StatusBadRequest) + } + params := database.CreateDiscordWebHookParams{ + ID: uuid.New(), + Url: _url, + Server: _server, + Channel: _channel, + Enabled: true, + } + s.Db.CreateDiscordWebHook(*s.ctx, params) + + bJson, err := json.Marshal(¶ms) + if err != nil { + log.Panicln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(bJson) +} diff --git a/routes/root.go b/routes/root.go index 4eeae7d..1f029ce 100644 --- a/routes/root.go +++ b/routes/root.go @@ -1,25 +1,50 @@ package routes import ( - "net/http" "fmt" + "net/http" + "github.com/go-chi/chi/v5" ) func RootRoutes() chi.Router { app := chi.NewRouter() - app.Get("/", func(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Hello World!")) - }) - - app.Get("/ping", func(w http.ResponseWriter, r *http.Request) { - msg := "pong" - w.Write([]byte(msg)) - }) - - app.Get("/hello/{world}", func(w http.ResponseWriter, r *http.Request) { - msg := fmt.Sprintf("Hello %v", chi.URLParam(r, "world")) - w.Write([]byte(msg)) + app.Route("/", func(r chi.Router) { + r.Get("/helloworld", helloWorld) + r.Get("/ping", ping) + r.Route("/hello/{who}", func(r chi.Router) { + r.Get("/", helloWho) + }) }) return app -} \ No newline at end of file +} + +// HelloWorld +// @Summary Responds back with "Hello world!" +// @Produce plain +// @Tags debug +// @Router /helloworld [get] +func helloWorld(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Hello World!")) +} + +// Ping +// @Summary Sends back "pong". Good to test with. +// @Produce plain +// @Tags debug +// @Router /ping [get] +func ping(w http.ResponseWriter, r *http.Request) { + msg := "pong" + w.Write([]byte(msg)) +} + +// HelloWho +// @Summary Responds back with "Hello x" depending on param passed in. +// @Param who path string true "Who" +// @Produce plain +// @Tags debug +// @Router /hello/{who} [get] +func helloWho(w http.ResponseWriter, r *http.Request) { + msg := fmt.Sprintf("Hello %v", chi.URLParam(r, "who")) + w.Write([]byte(msg)) +} diff --git a/routes/root_test.go b/routes/root_test.go new file mode 100644 index 0000000..da0d64f --- /dev/null +++ b/routes/root_test.go @@ -0,0 +1,2 @@ +package routes_test + diff --git a/routes/server.go b/routes/server.go new file mode 100644 index 0000000..a4d23a0 --- /dev/null +++ b/routes/server.go @@ -0,0 +1,108 @@ +package routes + +import ( + "context" + "database/sql" + //"net/http" + + "github.com/go-chi/chi/v5" + "github.com/go-chi/chi/v5/middleware" + httpSwagger "github.com/swaggo/http-swagger" + _ "github.com/lib/pq" + + "github.com/jtom38/newsbot/collector/database" + "github.com/jtom38/newsbot/collector/services/config" +) + +type Server struct { + Router *chi.Mux + Db *database.Queries + ctx *context.Context +} + +var ( + ErrIdValueMissing string = "id value is missing" + ErrValueNotUuid string = "a value given was expected to be a uuid but was not correct." + ErrNoRecordFound string = "no record was found." + ErrUnableToConvertToJson string = "Unable to convert to json" +) + +func NewServer(ctx context.Context) *Server { + s := &Server{ + ctx: &ctx, + } + + db, err := openDatabase(ctx) + if err != nil { + panic(err) + } + s.Db = db + + s.Router = chi.NewRouter() + s.MountMiddleware() + s.MountRoutes() + return s +} + +func openDatabase(ctx context.Context) (*database.Queries, error) { + _env := config.New() + connString := _env.GetConfig(config.Sql_Connection_String) + db, err := sql.Open("postgres", connString) + if err != nil { + panic(err) + } + + queries := database.New(db) + return queries, err +} + +func (s *Server) MountMiddleware() { + s.Router.Use(middleware.Logger) + s.Router.Use(middleware.Recoverer) +} + +func (s *Server) MountRoutes() { + s.Router.Get("/swagger/*", httpSwagger.Handler( + httpSwagger.URL("http://localhost:8081/swagger/doc.json"), //The url pointing to API definition + )) + + /* Root Routes */ + s.Router.Get("/api/helloworld", helloWorld) + s.Router.Get("/api/hello/{who}", helloWho) + s.Router.Get("/api/ping", ping) + + /* Article Routes */ + s.Router.Get("/api/articles", s.listArticles) + s.Router.Route("/api/articles/{ID}", func(r chi.Router) { + r.Get("/", s.getArticleById) + }) + s.Router.Get("/api/articles/by/sourceid", s.GetArticlesBySourceId) + + /* Discord Queue */ + s.Router.Get("/api/discord/queue", s.GetDiscordQueue) + + /* Discord WebHooks */ + s.Router.Post("/api/discord/webhooks/new", s.NewDiscordWebHook) + s.Router.Get("/api/discord/webhooks", s.GetDiscordWebHooks) + s.Router.Get("/api/discord/webhooks/byId", s.GetDiscordWebHooksById) + + /* Settings */ + s.Router.Get("/api/settings", s.getSettings) + + /* Source Routes */ + s.Router.Get("/api/config/sources", s.listSources) + s.Router.Post("/api/config/sources/new/reddit", s.newRedditSource) + s.Router.Post("/api/config/sources/new/youtube", s.newYoutubeSource) + s.Router.Post("/api/config/sources/new/twitch", s.newTwitchSource) + s.Router.Route("/api/config/sources/{ID}", func(r chi.Router) { + r.Get("/", s.getSources) + r.Delete("/", s.deleteSources) + r.Post("/disable", s.disableSource) + r.Post("/enable", s.enableSource) + }) + + /* Subscriptions */ + s.Router.Get("/api/subscriptions", s.ListSubscriptions) + s.Router.Get("/api/subscriptions/byDiscordId", s.GetSubscriptionsByDiscordId) + s.Router.Get("/api/subscriptions/bySourceId", s.GetSubscriptionsBySourceId) +} diff --git a/routes/settings.go b/routes/settings.go new file mode 100644 index 0000000..0a10936 --- /dev/null +++ b/routes/settings.go @@ -0,0 +1,44 @@ +package routes + +import ( + "encoding/json" + "log" + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" +) + +// 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 *Server) getSettings(w http.ResponseWriter, r *http.Request) { + //var item model.Sources + id := chi.URLParam(r, "ID") + + uuid, err := uuid.Parse(id) + if err != nil { + panic(err) + } + + res, err := s.Db.GetSourceByID(*s.ctx, uuid) + if err != nil { + panic(err) + } + + //itemId := fmt.Sprint(item.ID) + //if id != itemId { + // log.Panicln("Unable to find the requested record. Either unable to access SQL or the record does not exist.") + //} + + bResult, err := json.Marshal(res) + if err != nil { + log.Panicln(err) + } + + w.Header().Set("Content-Type", "application/json") + w.Write(bResult) +} \ No newline at end of file diff --git a/routes/sources.go b/routes/sources.go new file mode 100644 index 0000000..d0696e9 --- /dev/null +++ b/routes/sources.go @@ -0,0 +1,278 @@ +package routes + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "strings" + + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "github.com/jtom38/newsbot/collector/database" +) + +// ListSources +// @Summary Lists the top 50 records +// @Produce application/json +// @Tags config, source +// @Router /config/sources [get] +func (s *Server) listSources(w http.ResponseWriter, r *http.Request) { + //TODO Add top? + /* + top := chi.URLParam(r, "top") + topInt, err := strconv.ParseInt(top, 0, 32) + if err != nil { + panic(err) + } + res, err := s.Db.ListSources(*s.ctx, int32(topInt)) + */ + + res, err := s.Db.ListSources(*s.ctx, 50) + if err != nil { + http.Error(w, "url is missing a value", http.StatusBadRequest) + return + } + + bResult, err := json.Marshal(res) + if err != nil { + http.Error(w, "unable to convert to json", http.StatusBadRequest) + return + } + + w.Header().Set("Content-Type", "application/json") + w.Write(bResult) +} + +// GetSource +// @Summary Returns a single entity by ID +// @Param id path string true "uuid" +// @Produce application/json +// @Tags config, source +// @Router /config/sources/{id} [get] +func (s *Server) getSources(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "ID") + + uuid, err := uuid.Parse(id) + if err != nil { + http.Error(w, "id is not a uuid", http.StatusBadRequest) + return + } + + res, err := s.Db.GetSourceByID(*s.ctx, uuid) + if err != nil { + http.Error(w, "invalid id was given", http.StatusBadRequest) + panic(err) + } + + bResult, err := json.Marshal(res) + if err != nil { + log.Panicln(err) + } + + w.Header().Set("Content-Type", "application/json") + w.Write(bResult) +} + +// NewRedditSource +// @Summary Creates a new reddit source to monitor. +// @Param name query string true "name" +// @Param url query string true "url" +// @Tags config, source, reddit +// @Router /config/sources/new/reddit [post] +func (s *Server) newRedditSource(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + _name := query["name"][0] + _url := query["url"][0] + _tags := query["tags"][0] + + if _url == "" { + http.Error(w, "url is missing a value", http.StatusBadRequest) + return + } + if !strings.Contains(_url, "reddit.com") { + http.Error(w, "invalid url", http.StatusBadRequest) + return + } + + tags := fmt.Sprintf("reddit, %v, %v", _name, _tags) + params := database.CreateSourceParams{ + ID: uuid.New(), + Site: "reddit", + Name: _name, + Source: "reddit", + Type: "feed", + Enabled: true, + Url: _url, + Tags: tags, + } + s.Db.CreateSource(*s.ctx, params) + + bJson, err := json.Marshal(¶ms) + if err != nil { + log.Panicln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(bJson) +} + +// NewYoutubeSource +// @Summary Creates a new youtube source to monitor. +// @Param name query string true "name" +// @Param url query string true "url" +// @Param tags query string true "tags" +// @Tags config, source, youtube +// @Router /config/sources/new/youtube [post] +func (s *Server) newYoutubeSource(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + _name := query["name"][0] + _url := query["url"][0] + _tags := query["tags"][0] + + if _url == "" { + http.Error(w, "url is missing a value", http.StatusBadRequest) + return + } + if !strings.Contains(_url, "youtube.com") { + http.Error(w, "invalid url", http.StatusBadRequest) + return + } + + tags := fmt.Sprintf("youtube, %v, %v", _name, _tags) + params := database.CreateSourceParams{ + ID: uuid.New(), + Site: "youtube", + Name: _name, + Source: "youtube", + Type: "feed", + Enabled: true, + Url: _url, + Tags: tags, + } + s.Db.CreateSource(*s.ctx, params) + + bJson, err := json.Marshal(¶ms) + if err != nil { + log.Panicln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(bJson) +} + +// NewTwitchSource +// @Summary Creates a new twitch source to monitor. +// @Param name query string true "name" +// @Param url query string true "url" +// @Param tags query string true "tags" +// @Tags config, source, twitch +// @Router /config/sources/new/twitch [post] +func (s *Server) newTwitchSource(w http.ResponseWriter, r *http.Request) { + query := r.URL.Query() + _name := query["name"][0] + _url := query["url"][0] + _tags := query["tags"][0] + + if _url == "" { + http.Error(w, "url is missing a value", http.StatusBadRequest) + return + } + if !strings.Contains(_url, "twitch.tv") { + http.Error(w, "invalid url", http.StatusBadRequest) + return + } + + tags := fmt.Sprintf("twitch, %v, %v", _name, _tags) + params := database.CreateSourceParams{ + ID: uuid.New(), + Site: "twitch", + Name: _name, + Source: "twitch", + Type: "api", + Enabled: true, + Url: _url, + Tags: tags, + } + s.Db.CreateSource(*s.ctx, params) + + bJson, err := json.Marshal(¶ms) + if err != nil { + log.Panicln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(bJson) +} + +// DeleteSource +// @Summary Deletes a record by ID. +// @Param id path string true "id" +// @Tags config, source +// @Router /config/sources/{id} [delete] +func (s *Server) deleteSources(w http.ResponseWriter, r *http.Request) { + //var item model.Sources = model.Sources{} + + id := chi.URLParam(r, "ID") + uuid, err := uuid.Parse(id) + if err != nil { + log.Panicln(err) + } + + // Check to make sure we can find the record + _, err = s.Db.GetSourceByID(*s.ctx, uuid) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + // Delete the record + err = s.Db.DeleteSource(*s.ctx, uuid) + if err != nil { + log.Panic(err) + } +} + +// DisableSource +// @Summary Disables a source from processing. +// @Param id path string true "id" +// @Tags config, source +// @Router /config/sources/{id}/disable [post] +func (s *Server) disableSource(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "ID") + uuid, err := uuid.Parse(id) + if err != nil { + log.Panicln(err) + } + + // Check to make sure we can find the record + _, err = s.Db.GetSourceByID(*s.ctx, uuid) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + err = s.Db.DisableSource(*s.ctx, uuid) + if err != nil { + log.Panic(err) + } +} + +// EnableSource +// @Summary Enables a source to continue processing. +// @Param id path string true "id" +// @Tags config, source +// @Router /config/sources/{id}/enable [post] +func (s *Server) enableSource(w http.ResponseWriter, r *http.Request) { + id := chi.URLParam(r, "ID") + uuid, err := uuid.Parse(id) + if err != nil { + log.Panicln(err) + } + + // Check to make sure we can find the record + _, err = s.Db.GetSourceByID(*s.ctx, uuid) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + } + + err = s.Db.EnableSource(*s.ctx, uuid) + if err != nil { + log.Panic(err) + } +} diff --git a/routes/subscriptions.go b/routes/subscriptions.go new file mode 100644 index 0000000..d8b5578 --- /dev/null +++ b/routes/subscriptions.go @@ -0,0 +1,105 @@ +package routes + +import ( + "encoding/json" + "net/http" + + "github.com/google/uuid" +) + +// GetSubscriptions +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Tags config, Subscriptions +// @Router /subscriptions [get] +func (s *Server) ListSubscriptions(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + res, err := s.Db.ListSubscriptions(*s.ctx, 100) + if err != nil { + w.Write([]byte(err.Error())) + panic(err) + } + + bres, err := json.Marshal(res) + if err != nil { + http.Error(w, ErrUnableToConvertToJson, http.StatusBadRequest) + panic(err) + } + + w.Write(bres) +} + +// GetSubscriptionsByDiscordId +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Param id query string true "id" +// @Tags config, Subscriptions +// @Router /subscriptions/byDiscordId [get] +func (s *Server) GetSubscriptionsByDiscordId(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + query := r.URL.Query() + _id := query["id"][0] + if _id == "" { + http.Error(w, ErrIdValueMissing, http.StatusBadRequest) + return + } + + uuid, err := uuid.Parse(_id) + if err != nil { + http.Error(w, ErrValueNotUuid, http.StatusBadRequest) + return + } + + res, err := s.Db.GetSubscriptionsByDiscordWebHookId(*s.ctx, uuid) + if err != nil { + http.Error(w, ErrNoRecordFound, http.StatusBadRequest) + return + } + + bres, err := json.Marshal(res) + if err != nil { + http.Error(w, ErrUnableToConvertToJson, http.StatusBadRequest) + return + } + + w.Write(bres) +} + +// GetSubscriptionsBySourceId +// @Summary Returns the top 100 entries from the queue to be processed. +// @Produce application/json +// @Param id query string true "id" +// @Tags config, Subscriptions +// @Router /subscriptions/bySourceId [get] +func (s *Server) GetSubscriptionsBySourceId(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + + query := r.URL.Query() + _id := query["id"][0] + if _id == "" { + http.Error(w, ErrIdValueMissing, http.StatusBadRequest) + return + } + + uuid, err := uuid.Parse(_id) + if err != nil { + http.Error(w, ErrValueNotUuid, http.StatusBadRequest) + return + } + + res, err := s.Db.GetSubscriptionsByDiscordWebHookId(*s.ctx, uuid) + if err != nil { + http.Error(w, ErrNoRecordFound, http.StatusBadRequest) + return + } + + bres, err := json.Marshal(res) + if err != nil { + http.Error(w, ErrUnableToConvertToJson, http.StatusBadRequest) + return + } + + w.Write(bres) +} \ No newline at end of file diff --git a/services/cron/scheduler.go b/services/cron/scheduler.go index 7fdcbaf..b94f3d1 100644 --- a/services/cron/scheduler.go +++ b/services/cron/scheduler.go @@ -15,40 +15,56 @@ import ( "github.com/jtom38/newsbot/collector/services/config" ) -var _env config.ConfigClient -var _connString string -var _queries *database.Queries - -func EnableScheduler(ctx context.Context) { - c := cron.New() - OpenDatabase(ctx) - - //c.AddFunc("*/5 * * * *", func() { go CheckCache() }) - c.AddFunc("* */1 * * *", func() { go CheckReddit(ctx) }) - //c.AddFunc("* */1 * * *", func() { go CheckYoutube() }) - //c.AddFunc("* */1 * * *", func() { go CheckFfxiv() }) - //c.AddFunc("* */1 * * *", func() { go CheckTwitch() }) - - c.Start() +type Cron struct { + Db *database.Queries + ctx *context.Context + timer *cron.Cron } -// Open the connection to the database and share it with the package so all of them are able to share. -func OpenDatabase(ctx context.Context) error { - _env = config.New() - _connString = _env.GetConfig(config.Sql_Connection_String) - db, err := sql.Open("postgres", _connString) +func openDatabase() (*database.Queries, error) { + _env := config.New() + connString := _env.GetConfig(config.Sql_Connection_String) + db, err := sql.Open("postgres", connString) if err != nil { panic(err) } queries := database.New(db) - _queries = queries - return err + return queries, err +} + +func New(ctx context.Context) *Cron { + c := &Cron{ + ctx: &ctx, + } + + timer := cron.New() + queries, err := openDatabase() + if err != nil { + panic(err) + } + c.Db = queries + + //timer.AddFunc("*/5 * * * *", func() { go CheckCache() }) + //timer.AddFunc("* */30 * * *", func() { go c.CheckReddit(ctx) }) + //timer.AddFunc("* */1 * * *", func() { go CheckYoutube() }) + //timer.AddFunc("* */1 * * *", func() { go CheckFfxiv() }) + //timer.AddFunc("* */1 * * *", func() { go CheckTwitch() }) + c.timer = timer + return c +} + +func (c *Cron) Start() { + c.timer.Start() +} + +func (c *Cron) Stop() { + c.timer.Stop() } // This is the main entry point to query all the reddit services -func CheckReddit(ctx context.Context) { - sources, err := _queries.ListSourcesBySource(ctx, "reddit") +func (c *Cron) CheckReddit(ctx context.Context) { + sources, err := c.Db.ListSourcesBySource(*c.ctx, "reddit") if err != nil { log.Printf("No defines sources for reddit to query - %v\r", err) } @@ -63,13 +79,13 @@ func CheckReddit(ctx context.Context) { log.Println(err) } redditArticles := rc.ConvertToArticles(raw) - checkPosts(ctx, redditArticles) + c.checkPosts(*c.ctx, redditArticles) } } -func CheckYoutube(ctx context.Context) { +func (c *Cron) CheckYoutube(ctx context.Context) { // Add call to the db to request youtube sources. - sources, err := _queries.ListSourcesBySource(ctx, "youtube") + sources, err := c.Db.ListSourcesBySource(*c.ctx, "youtube") if err != nil { log.Printf("Youtube - No sources found to query - %v\r", err) } @@ -83,12 +99,12 @@ func CheckYoutube(ctx context.Context) { if err != nil { log.Println(err) } - checkPosts(ctx, raw) + c.checkPosts(*c.ctx, raw) } } -func CheckFfxiv(ctx context.Context) { - sources, err := _queries.ListSourcesBySource(ctx, "ffxiv") +func (c *Cron) CheckFfxiv(ctx context.Context) { + sources, err := c.Db.ListSourcesBySource(*c.ctx, "ffxiv") if err != nil { log.Printf("Final Fantasy XIV - No sources found to query - %v\r", err) } @@ -102,12 +118,12 @@ func CheckFfxiv(ctx context.Context) { if err != nil { log.Println(err) } - checkPosts(ctx, items) + c.checkPosts(*c.ctx, items) } } -func CheckTwitch(ctx context.Context) error { - sources, err := _queries.ListSourcesBySource(ctx, "twitch") +func (c *Cron) CheckTwitch(ctx context.Context) error { + sources, err := c.Db.ListSourcesBySource(*c.ctx, "twitch") if err != nil { log.Printf("Twitch - No sources found to query - %v\r", err) } @@ -126,17 +142,17 @@ func CheckTwitch(ctx context.Context) error { if err != nil { log.Println(err) } - checkPosts(ctx, items) + c.checkPosts(*c.ctx, items) } return nil } -func checkPosts(ctx context.Context, posts []database.Article) { +func (c *Cron) checkPosts(ctx context.Context, posts []database.Article) { for _, item := range posts { - _, err := _queries.GetArticleByUrl(ctx, item.Url) + _, err := c.Db.GetArticleByUrl(*c.ctx, item.Url) if err != nil { - err = postArticle(ctx, item) + err = c.postArticle(ctx, item) if err != nil { log.Printf("Reddit - Failed to post article - %v - %v.\r", item.Url, err) } else { @@ -147,8 +163,8 @@ func checkPosts(ctx context.Context, posts []database.Article) { time.Sleep(30 * time.Second) } -func postArticle(ctx context.Context, item database.Article) error { - err := _queries.CreateArticle(ctx, database.CreateArticleParams{ +func (c *Cron) postArticle(ctx context.Context, item database.Article) error { + err := c.Db.CreateArticle(*c.ctx, database.CreateArticleParams{ ID: uuid.New(), Sourceid: item.Sourceid, Tags: item.Tags, diff --git a/services/cron/scheduler_test.go b/services/cron/scheduler_test.go index 312b347..d2e84bf 100644 --- a/services/cron/scheduler_test.go +++ b/services/cron/scheduler_test.go @@ -14,23 +14,20 @@ func TestInvokeTwitch(t *testing.T) { // TODO add database mocks but not sure how to do that yet. func TestCheckReddit(t *testing.T) { ctx := context.Background() - cron.OpenDatabase(ctx) - cron.CheckReddit(ctx) + c := cron.Cron{} + c.CheckReddit(ctx) } func TestCheckYouTube(t *testing.T) { ctx := context.Background() - cron.OpenDatabase(ctx) - cron.CheckYoutube(ctx) + c := cron.Cron{} + c.CheckYoutube(ctx) } func TestCheckTwitch(t *testing.T) { ctx := context.Background() - err := cron.OpenDatabase(ctx) - if err != nil { - t.Error(err) - } - err = cron.CheckTwitch(ctx) + c := cron.Cron{} + err := c.CheckTwitch(ctx) if err != nil { t.Error(err) }