diff --git a/Makefile b/Makefile index 8750eba..996b559 100644 --- a/Makefile +++ b/Makefile @@ -26,12 +26,12 @@ tui: protobuf protobuf: mkdir -p $(PROTOBUF_API_DEST) - protoc --go_out=. ./proto/* + protoc --go_out=./api --go_opt=paths=source_relative --go-grpc_out=./api --go-grpc_opt=paths=source_relative proto/* test: protobuf - go test -race ./... + find . -name go.mod -execdir go test ./... \; -all: bot-dev bot-prod sever tui test +all: bot-prod sever tui test # FIXME Replaced invalid path install: prod test diff --git a/bot/bot.go b/bot/bot.go index 86ca626..bfe31be 100644 --- a/bot/bot.go +++ b/bot/bot.go @@ -12,6 +12,7 @@ import ( "github.com/wittano/komputer/bot/log" "github.com/wittano/komputer/bot/voice" "github.com/wittano/komputer/db" + dbJoke "github.com/wittano/komputer/db/joke" "log/slog" "time" ) @@ -34,7 +35,7 @@ func (sc slashCommandHandler) handleSlashCommand(s *discordgo.Session, i *discor requestID := uuid.New().String() loggerCtx := log.NewContext(requestID) deadlineCtx, cancel := context.WithTimeout(loggerCtx, cmdTimeout) - ctx := context.WithValue(deadlineCtx, joke.GuildIDKey, i.GuildID) + ctx := context.WithValue(deadlineCtx, dbJoke.GuildIDKey, i.GuildID) defer cancel() userID := i.Member.User.ID @@ -123,8 +124,8 @@ func (d *DiscordBot) Close() (err error) { return } -func createJokeGetServices(globalCtx context.Context, database *db.MongodbDatabase) []joke.SearchService { - return []joke.SearchService{ +func createJokeGetServices(globalCtx context.Context, database *db.MongodbDatabase) []dbJoke.SearchService { + return []dbJoke.SearchService{ jokeDevServiceID: joke.NewJokeDevService(globalCtx), humorAPIServiceID: joke.NewHumorAPIService(globalCtx), databaseServiceID: joke.NewDatabaseJokeService(database), @@ -133,12 +134,12 @@ func createJokeGetServices(globalCtx context.Context, database *db.MongodbDataba func createCommands( globalCtx context.Context, - services []joke.SearchService, + services []dbJoke.SearchService, spockVoice map[string]chan struct{}, guildVoiceChats map[string]voice.ChatInfo, ) map[string]command.DiscordSlashCommandHandler { welcome := command.WelcomeCommand{} - addJoke := command.AddJokeCommand{Service: services[databaseServiceID].(joke.DatabaseService)} + addJoke := command.AddJokeCommand{Service: services[databaseServiceID].(dbJoke.Database)} getJoke := command.JokeCommand{Services: services} spock := command.SpockCommand{ GlobalCtx: globalCtx, @@ -159,7 +160,7 @@ func createCommands( } func createOptions( - services []joke.SearchService, + services []dbJoke.SearchService, commands map[string]command.DiscordSlashCommandHandler, ) []command.DiscordEventHandler { apologies := command.ApologiesOption{} diff --git a/bot/command/joke.go b/bot/command/joke.go index 7a6e498..f56eb0c 100644 --- a/bot/command/joke.go +++ b/bot/command/joke.go @@ -5,8 +5,8 @@ import ( "errors" "fmt" "github.com/bwmarrin/discordgo" - "github.com/wittano/komputer/bot/joke" "github.com/wittano/komputer/bot/log" + "github.com/wittano/komputer/db/joke" "go.mongodb.org/mongo-driver/bson/primitive" "log/slog" "math/rand" @@ -48,7 +48,7 @@ func (j JokeCommand) Command() *discordgo.ApplicationCommand { jokeTypeOption(false), { Name: idOptionKey, - Description: "Joke ID", + Description: "Jokes ID", Type: discordgo.ApplicationCommandOptionString, Required: false, }, @@ -73,7 +73,7 @@ findJoke: return nil, DiscordError{err, "Nie udało mi się, znaleść żadnego żartu"} } - res, err := service.Joke(ctx, searchQuery) + res, err := service.RandomJoke(ctx, searchQuery) if err != nil { loggerCtx.Logger.Error(err.Error()) goto findJoke @@ -137,7 +137,7 @@ func jokeCategoryOption(required bool) *discordgo.ApplicationCommandOption { return &discordgo.ApplicationCommandOption{ Type: discordgo.ApplicationCommandOptionString, Name: categoryOptionKey, - Description: "Joke category", + Description: "Jokes category", Required: required, Choices: []*discordgo.ApplicationCommandOptionChoice{ { @@ -164,7 +164,7 @@ func jokeTypeOption(required bool) *discordgo.ApplicationCommandOption { return &discordgo.ApplicationCommandOption{ Type: discordgo.ApplicationCommandOptionString, Name: typeOptionKey, - Description: "Type of joke", + Description: "RawType of joke", Required: required, Choices: []*discordgo.ApplicationCommandOptionChoice{ { @@ -200,7 +200,7 @@ func (j discordJoke) singleTypeJoke() (msg *discordgo.InteractionResponseData) { embeds := []*discordgo.MessageEmbed{ { Type: discordgo.EmbedTypeRich, - Title: "Joke", + Title: "Jokes", Description: j.joke.Answer, Color: 0x02f5f5, Author: &discordgo.MessageEmbedAuthor{ @@ -234,7 +234,7 @@ func (j discordJoke) twoPartJoke() *discordgo.InteractionResponseData { embeds := []*discordgo.MessageEmbed{ { Type: discordgo.EmbedTypeRich, - Title: "Joke", + Title: "Jokes", Color: 0x02f5f5, Author: &discordgo.MessageEmbedAuthor{ Name: "komputer", @@ -382,7 +382,7 @@ func (n NextJokeOption) Execute(ctx context.Context, _ *discordgo.Session, i *di return nil, err } - res, err := service.Joke(ctx, joke.SearchParams{Type: randJokeType()}) + res, err := service.RandomJoke(ctx, joke.SearchParams{Type: randJokeType()}) if err != nil { return nil, err } @@ -416,7 +416,7 @@ func (s SameJokeCategoryOption) Execute(ctx context.Context, _ *discordgo.Sessio return nil, err } - res, err := service.Joke(ctx, joke.SearchParams{Type: randJokeType(), Category: category}) + res, err := service.RandomJoke(ctx, joke.SearchParams{Type: randJokeType(), Category: category}) if err != nil { return nil, err } diff --git a/bot/command/joke_test.go b/bot/command/joke_test.go index fe4f7b3..92a8e26 100644 --- a/bot/command/joke_test.go +++ b/bot/command/joke_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "github.com/wittano/komputer/bot/joke" + dbJoke "github.com/wittano/komputer/db/joke" "go.mongodb.org/mongo-driver/mongo" "testing" ) @@ -20,7 +21,7 @@ func (d dumpMongoService) Client(_ context.Context) (*mongo.Client, error) { func TestSelectGetService(t *testing.T) { ctx := context.Background() - testServices := []joke.SearchService{ + testServices := []dbJoke.SearchService{ joke.NewJokeDevService(ctx), joke.NewHumorAPIService(ctx), joke.NewDatabaseJokeService(dumpMongoService{}), @@ -43,7 +44,7 @@ func TestSelectGetService(t *testing.T) { func TestFindJokeService_ContextCancelled(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - testServices := []joke.SearchService{ + testServices := []dbJoke.SearchService{ joke.NewJokeDevService(ctx), joke.NewHumorAPIService(ctx), joke.NewDatabaseJokeService(dumpMongoService{}), @@ -56,7 +57,7 @@ func TestFindJokeService_ContextCancelled(t *testing.T) { func TestFindJokeService_ServicesIsDeactivated(t *testing.T) { ctx := context.Background() - services := []joke.SearchService{ + services := []dbJoke.SearchService{ joke.NewDatabaseJokeService(dumpMongoService{}), } diff --git a/bot/joke/humorapi.go b/bot/joke/humorapi.go index 08599a0..2b8547a 100644 --- a/bot/joke/humorapi.go +++ b/bot/joke/humorapi.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/wittano/komputer/bot/log" + "github.com/wittano/komputer/db/joke" "io" "log/slog" "net/http" @@ -43,25 +44,25 @@ func (h HumorAPIService) Active(ctx context.Context) (active bool) { return } -func (h *HumorAPIService) Joke(ctx context.Context, search SearchParams) (Joke, error) { +func (h *HumorAPIService) RandomJoke(ctx context.Context, search joke.SearchParams) (joke.Joke, error) { select { case <-ctx.Done(): - return Joke{}, context.Canceled + return joke.Joke{}, context.Canceled default: } if !h.Active(ctx) { - return Joke{}, HumorAPILimitExceededErr + return joke.Joke{}, HumorAPILimitExceededErr } apiKey, ok := os.LookupEnv(humorAPIKey) if !ok { - return Joke{}, errors.New("humorAPI: missing " + humorAPIKey) + return joke.Joke{}, errors.New("humorAPI: missing " + humorAPIKey) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, humorApiURL+humorAPICategory(search.Category), nil) if err != nil { - return Joke{}, err + return joke.Joke{}, err } req.Header["X-RapidAPI-Key"] = []string{apiKey} @@ -69,7 +70,7 @@ func (h *HumorAPIService) Joke(ctx context.Context, search SearchParams) (Joke, res, err := h.client.Do(req) if err != nil { - return Joke{}, err + return joke.Joke{}, err } defer res.Body.Close() @@ -81,7 +82,7 @@ func (h *HumorAPIService) Joke(ctx context.Context, search SearchParams) (Joke, go unlockService(h.globalCtx, &h.active, resetTime) - return Joke{}, HumorAPILimitExceededErr + return joke.Joke{}, HumorAPILimitExceededErr } else if res.StatusCode != http.StatusOK { msg, err := io.ReadAll(res.Body) if err != nil { @@ -91,41 +92,41 @@ func (h *HumorAPIService) Joke(ctx context.Context, search SearchParams) (Joke, msg = []byte{} } - return Joke{}, fmt.Errorf("humorAPI: failed to get joke. status '%d', msg: '%s'", res.StatusCode, msg) + return joke.Joke{}, fmt.Errorf("humorAPI: failed to get joke. status '%d', msg: '%s'", res.StatusCode, msg) } select { case <-ctx.Done(): - return Joke{}, context.Canceled + return joke.Joke{}, context.Canceled default: } resBody, err := io.ReadAll(res.Body) if err != nil { - return Joke{}, err + return joke.Joke{}, err } - var joke humorAPIResponse - if err = json.Unmarshal(resBody, &joke); err != nil { - return Joke{}, err + var humorRes humorAPIResponse + if err = json.Unmarshal(resBody, &humorRes); err != nil { + return joke.Joke{}, err } - return Joke{ + return joke.Joke{ Category: search.Category, - Type: Single, - Answer: joke.Content, + Type: joke.Single, + Answer: humorRes.Content, }, nil } -func humorAPICategory(category Category) (res string) { +func humorAPICategory(category joke.Category) (res string) { switch category { - case PROGRAMMING: + case joke.PROGRAMMING: res = "nerdy" - case DARK: + case joke.DARK: res = "dark" - case YOMAMA: + case joke.YOMAMA: res = "yo_mama" - case Any: + case joke.Any: default: res = "one_liner" } diff --git a/bot/joke/humorapi_test.go b/bot/joke/humorapi_test.go index a52b96a..7a36948 100644 --- a/bot/joke/humorapi_test.go +++ b/bot/joke/humorapi_test.go @@ -5,12 +5,27 @@ import ( "encoding/json" "errors" "github.com/jarcoal/httpmock" + "github.com/wittano/komputer/db/joke" "net/http" "os" "strconv" "testing" ) +var ( + testParams = joke.SearchParams{ + Type: joke.Single, + Category: joke.Any, + } + testJoke = joke.Joke{ + Question: "testQuestion", + Answer: "testAnswer", + Type: joke.Single, + Category: joke.Any, + GuildID: "", + } +) + var testHumorAPIResponse = humorAPIResponse{ Content: "testJokeRes", ID: 213, @@ -36,7 +51,7 @@ func TestHumorAPIService_Get(t *testing.T) { ctx := context.Background() service := NewHumorAPIService(ctx) - joke, err := service.Joke(ctx, testParams) + joke, err := service.RandomJoke(ctx, testParams) if err != nil { t.Fatal(err) } @@ -53,7 +68,7 @@ func TestHumorAPIService_Get(t *testing.T) { func TestHumorAPIService_GetWithMissingApiKey(t *testing.T) { ctx := context.Background() service := NewHumorAPIService(ctx) - if _, err := service.Joke(ctx, testParams); err == nil { + if _, err := service.RandomJoke(ctx, testParams); err == nil { t.Fatal("service found API key, but it didn't set") } } @@ -79,7 +94,7 @@ func TestHumorAPIService_GetWithApiReturnInvalidStatus(t *testing.T) { service := NewHumorAPIService(ctx) - if _, err := service.Joke(ctx, testParams); err == nil { + if _, err := service.RandomJoke(ctx, testParams); err == nil { t.Fatal("service didn't handle correct a bad/invalid http status") } }) @@ -101,7 +116,7 @@ func TestHumorAPIService_GetWithApiLimitExceeded(t *testing.T) { ctx := context.Background() service := NewHumorAPIService(ctx) - if _, err := service.Joke(ctx, testParams); !errors.Is(err, HumorAPILimitExceededErr) { + if _, err := service.RandomJoke(ctx, testParams); !errors.Is(err, HumorAPILimitExceededErr) { t.Fatal(err) } } @@ -123,11 +138,11 @@ func TestHumorAPIService_Active(t *testing.T) { service := NewHumorAPIService(ctx) - if _, err := service.Joke(ctx, testParams); !errors.Is(err, HumorAPILimitExceededErr) { + if _, err := service.RandomJoke(ctx, testParams); !errors.Is(err, HumorAPILimitExceededErr) { t.Fatal(err) } - if _, err := service.Joke(ctx, testParams); !errors.Is(err, HumorAPILimitExceededErr) { + if _, err := service.RandomJoke(ctx, testParams); !errors.Is(err, HumorAPILimitExceededErr) { t.Fatal(err) } diff --git a/bot/joke/jokedev.go b/bot/joke/jokedev.go index ed1525d..b258cd1 100644 --- a/bot/joke/jokedev.go +++ b/bot/joke/jokedev.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/wittano/komputer/db/joke" "io" "net/http" "time" @@ -16,7 +17,7 @@ const ( ) type mapper interface { - Joke() Joke + Joke() joke.Joke } type flags struct { @@ -39,11 +40,11 @@ type singleResponse struct { Content string `json:"joke"` } -func (j singleResponse) Joke() Joke { - return Joke{ +func (j singleResponse) Joke() joke.Joke { + return joke.Joke{ Answer: j.Content, - Type: Single, - Category: Category(j.Category), + Type: joke.Single, + Category: joke.Category(j.Category), } } @@ -59,12 +60,12 @@ type twoPartResponse struct { Delivery string `json:"delivery"` } -func (j twoPartResponse) Joke() Joke { - return Joke{ +func (j twoPartResponse) Joke() joke.Joke { + return joke.Joke{ Question: j.Setup, Answer: j.Delivery, - Type: Single, - Category: Category(j.Category), + Type: joke.Single, + Category: joke.Category(j.Category), } } @@ -87,30 +88,30 @@ func (d DevService) Active(ctx context.Context) (active bool) { return } -func (d *DevService) Joke(ctx context.Context, params SearchParams) (Joke, error) { +func (d *DevService) RandomJoke(ctx context.Context, params joke.SearchParams) (joke.Joke, error) { select { case <-ctx.Done(): - return Joke{}, context.Canceled + return joke.Joke{}, context.Canceled default: } if !d.Active(ctx) { - return Joke{}, DevServiceLimitExceededErr + return joke.Joke{}, DevServiceLimitExceededErr } - if params.Category == YOMAMA || params.Category == "" { - params.Category = Any + if params.Category == joke.YOMAMA || params.Category == "" { + params.Category = joke.Any } url := fmt.Sprintf(jokeDevAPIUrlTemplate, params.Category, params.Type) req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { - return Joke{}, err + return joke.Joke{}, err } res, err := d.client.Do(req) if err != nil { - return Joke{}, err + return joke.Joke{}, err } defer res.Body.Close() @@ -125,27 +126,27 @@ func (d *DevService) Joke(ctx context.Context, params SearchParams) (Joke, error go unlockService(d.globalCtx, &d.active, resetTime) - return Joke{}, DevServiceLimitExceededErr + return joke.Joke{}, DevServiceLimitExceededErr } if res.StatusCode >= 400 { - return Joke{}, errors.New("jokedev: client or server side error") + return joke.Joke{}, errors.New("jokedev: client or server side error") } resBody, err := io.ReadAll(res.Body) if err != nil { - return Joke{}, err + return joke.Joke{}, err } var mapper mapper switch params.Type { - case Single: + case joke.Single: singleRes := singleResponse{} err = json.Unmarshal(resBody, &singleRes) mapper = singleRes - case TwoPart: + case joke.TwoPart: twoPartRes := &twoPartResponse{} err = json.Unmarshal(resBody, &twoPartRes) @@ -154,7 +155,7 @@ func (d *DevService) Joke(ctx context.Context, params SearchParams) (Joke, error } if err != nil { - return Joke{}, err + return joke.Joke{}, err } return mapper.Joke(), nil diff --git a/bot/joke/jokedev_test.go b/bot/joke/jokedev_test.go index c58d317..3088aad 100644 --- a/bot/joke/jokedev_test.go +++ b/bot/joke/jokedev_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "github.com/jarcoal/httpmock" + "github.com/wittano/komputer/db/joke" "net/http" "os" "strconv" @@ -45,17 +46,17 @@ func TestDevService_Get(t *testing.T) { ctx := context.Background() service := NewJokeDevService(ctx) - joke, err := service.Joke(ctx, testParams) + res, err := service.RandomJoke(ctx, testParams) if err != nil { t.Fatal(err) } - if joke.Answer != testSingleJokeDev.Content { - t.Fatalf("Invalid joke response. Expected: '%s', Result: '%s'", testSingleJokeDev.Content, joke.Answer) + if res.Answer != testSingleJokeDev.Content { + t.Fatalf("Invalid res response. Expected: '%s', Result: '%s'", testSingleJokeDev.Content, res.Answer) } - if joke.Category != Category(testSingleJokeDev.Category) { - t.Fatalf("Invalid category. Expected: '%s', Result: '%s'", testParams, joke.Category) + if res.Category != joke.Category(testSingleJokeDev.Category) { + t.Fatalf("Invalid category. Expected: '%s', Result: '%s'", testParams, res.Category) } } @@ -80,7 +81,7 @@ func TestDevService_GetAndApiReturnInvalidStatus(t *testing.T) { service := NewJokeDevService(ctx) - if _, err := service.Joke(ctx, testParams); err == nil { + if _, err := service.RandomJoke(ctx, testParams); err == nil { t.Fatal("service didn't handle correct a bad/invalid http status") } }) @@ -102,7 +103,7 @@ func TestDevService_GetWithApiLimitExceeded(t *testing.T) { ctx := context.Background() service := NewJokeDevService(ctx) - if _, err := service.Joke(ctx, testParams); !errors.Is(err, DevServiceLimitExceededErr) { + if _, err := service.RandomJoke(ctx, testParams); !errors.Is(err, DevServiceLimitExceededErr) { t.Fatal(err) } } @@ -122,11 +123,11 @@ func TestDevService_Active(t *testing.T) { service := NewJokeDevService(ctx) - if _, err := service.Joke(ctx, testParams); !errors.Is(err, DevServiceLimitExceededErr) { + if _, err := service.RandomJoke(ctx, testParams); !errors.Is(err, DevServiceLimitExceededErr) { t.Fatal(err) } - if _, err := service.Joke(ctx, testParams); !errors.Is(err, DevServiceLimitExceededErr) { + if _, err := service.RandomJoke(ctx, testParams); !errors.Is(err, DevServiceLimitExceededErr) { t.Fatal(err) } diff --git a/bot/joke/types.go b/bot/joke/types.go index 7607939..2d681e2 100644 --- a/bot/joke/types.go +++ b/bot/joke/types.go @@ -3,31 +3,19 @@ package joke import ( "context" "github.com/wittano/komputer/db" + "github.com/wittano/komputer/db/joke" "net/http" "os" "time" ) -type AddService interface { - Add(ctx context.Context, joke Joke) (string, error) -} - -type SearchService interface { - Joke(ctx context.Context, search SearchParams) (Joke, error) - ActiveChecker -} - -type ActiveChecker interface { - Active(ctx context.Context) bool -} - -func NewJokeDevService(globalCtx context.Context) SearchService { +func NewJokeDevService(globalCtx context.Context) joke.SearchService { client := http.Client{Timeout: time.Second * 1} return &DevService{client, true, globalCtx} } -func NewHumorAPIService(globalCtx context.Context) SearchService { +func NewHumorAPIService(globalCtx context.Context) joke.SearchService { client := http.Client{Timeout: time.Second * 1} env, ok := os.LookupEnv(humorAPIKey) @@ -36,6 +24,24 @@ func NewHumorAPIService(globalCtx context.Context) SearchService { return &HumorAPIService{client, active, globalCtx} } -func NewDatabaseJokeService(database db.MongodbService) DatabaseService { - return DatabaseService{mongodb: database} +func NewDatabaseJokeService(database db.MongodbService) joke.Database { + return joke.Database{Mongodb: database} +} + +func unlockService(ctx context.Context, activeFlag *bool, resetTime time.Time) { + deadlineCtx, cancel := context.WithDeadline(ctx, resetTime) + defer cancel() + + for { + if *activeFlag { + return + } + + select { + case <-deadlineCtx.Done(): + *activeFlag = true + return + default: + } + } } diff --git a/cmd/server/main.go b/cmd/server/main.go index b1b14d0..c8c0516 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -1,7 +1,21 @@ package main -import "fmt" +import ( + "flag" + "log" + "server" +) func main() { - fmt.Println("Hello World!") + port := flag.Uint64("port", 8080, "Server TCP port") + + s, err := server.New(*port) + if err != nil { + log.Fatalf("failed initialized server: %s", err) + } + defer s.Close() + + if err = s.Start(); err != nil { + log.Fatal(err) + } } diff --git a/bot/joke/database.go b/db/joke/database.go similarity index 54% rename from bot/joke/database.go rename to db/joke/database.go index 29e39d0..ce71693 100644 --- a/bot/joke/database.go +++ b/db/joke/database.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + komputer "github.com/wittano/komputer/api/proto" "github.com/wittano/komputer/db" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -18,50 +19,17 @@ const ( GuildIDKey = "guildID" ) -type ( - Type string - Category string -) - -const ( - Single Type = "single" - TwoPart Type = "twopart" -) - -const ( - PROGRAMMING Category = "Programming" - MISC Category = "Misc" - DARK Category = "Dark" - YOMAMA Category = "YoMama" - Any Category = "Any" -) - -type Joke struct { - ID primitive.ObjectID `bson:"_id"` - Question string `bson:"question"` - Answer string `bson:"answer"` - Type Type `bson:"type"` - Category Category `bson:"category"` - GuildID string `bson:"guild_id"` -} - -type SearchParams struct { - Type Type - Category Category - ID primitive.ObjectID -} - -type DatabaseService struct { - mongodb db.MongodbService +type Database struct { + Mongodb db.MongodbService } -func (d DatabaseService) Active(ctx context.Context) bool { +func (d Database) Active(ctx context.Context) bool { const maxTimeoutTime = 500 ctx, cancel := context.WithTimeout(ctx, maxTimeoutTime*time.Millisecond) defer cancel() - client, err := d.mongodb.Client(ctx) + client, err := d.Mongodb.Client(ctx) if err != nil { return false } @@ -74,14 +42,14 @@ func (d DatabaseService) Active(ctx context.Context) bool { return true } -func (d DatabaseService) Add(ctx context.Context, joke Joke) (string, error) { +func (d Database) Add(ctx context.Context, joke Joke) (string, error) { select { case <-ctx.Done(): return "", context.Canceled default: } - mongodb, err := d.mongodb.Client(ctx) + mongodb, err := d.Mongodb.Client(ctx) if err != nil { return "", err } @@ -94,21 +62,38 @@ func (d DatabaseService) Add(ctx context.Context, joke Joke) (string, error) { return res.InsertedID.(primitive.ObjectID).Hex(), nil } -// Joke Try to find Joke from Mongodb database. If SearchParams is empty, then function will find 1 random joke -func (d DatabaseService) Joke(ctx context.Context, search SearchParams) (Joke, error) { +func (d Database) RandomJoke(ctx context.Context, search SearchParams) (Joke, error) { + jokes, err := d.Jokes(ctx, search, nil) + if err != nil { + return Joke{}, err + } + + return jokes[rand.Int()%len(jokes)], nil +} + +func (d Database) Joke(ctx context.Context, search SearchParams) (Joke, error) { + jokes, err := d.Jokes(ctx, search, nil) + if err != nil { + return Joke{}, err + } + + return jokes[0], nil +} + +func (d Database) Jokes(ctx context.Context, search SearchParams, page *komputer.Pagination) ([]Joke, error) { select { case <-ctx.Done(): - return Joke{}, context.Canceled + return nil, context.Canceled default: } if !d.Active(ctx) { - return Joke{}, errors.New("databases isn't responding") + return nil, errors.New("databases isn't responding") } - mongodb, err := d.mongodb.Client(ctx) + mongodb, err := d.Mongodb.Client(ctx) if err != nil { - return Joke{}, err + return nil, err } if search.Category == "" { @@ -122,11 +107,26 @@ func (d DatabaseService) Joke(ctx context.Context, search SearchParams) (Joke, e // Create query to database const matchQueryKey = "$match" + var ( + pageSize uint32 = 10 + pageNr uint32 = 0 + ) + + if page != nil { + if page.Size > 0 { + pageSize = page.Size + } + if page.Page > 0 { + pageNr = page.Page + } + } + pipeline := mongo.Pipeline{{{ "$sample", bson.D{{ - "size", 10, + "size", pageSize, }}, }}, + {{Key: "$skip", Value: pageNr * pageSize}}, {{ matchQueryKey, bson.D{ { @@ -158,36 +158,42 @@ func (d DatabaseService) Joke(ctx context.Context, search SearchParams) (Joke, e // SearchParams res, err := mongodb.Database(db.DatabaseName).Collection(collectionName).Aggregate(ctx, pipeline) if err != nil { - return Joke{}, err + return nil, err } defer res.Close(ctx) var jokes []Joke if err = res.All(ctx, &jokes); err != nil { - return Joke{}, err + return nil, err } if len(jokes) == 0 { - return Joke{}, fmt.Errorf("jokes with category '%s', type '%s' wasn't found", search.Category, search.Type) + return nil, fmt.Errorf("jokes with category '%s', type '%s' wasn't found", search.Category, search.Type) } - return jokes[rand.Int()%len(jokes)], nil + return jokes, nil } -func unlockService(ctx context.Context, activeFlag *bool, resetTime time.Time) { - deadlineCtx, cancel := context.WithDeadline(ctx, resetTime) - defer cancel() +func (d Database) Remove(ctx context.Context, id string) error { + select { + case <-ctx.Done(): + return context.Canceled + default: + } - for { - if *activeFlag { - return - } + if !d.Active(ctx) { + return errors.New("databases isn't responding") + } - select { - case <-deadlineCtx.Done(): - *activeFlag = true - return - default: - } + client, err := d.Mongodb.Client(ctx) + if err != nil { + return err } + + filter := bson.D{{"$match", bson.D{{ + "_id", id, + }}}} + + _, err = client.Database(db.DatabaseName).Collection(collectionName).DeleteOne(ctx, filter) + return err } diff --git a/bot/joke/database_test.go b/db/joke/database_test.go similarity index 69% rename from bot/joke/database_test.go rename to db/joke/database_test.go index ee5e78e..218298b 100644 --- a/bot/joke/database_test.go +++ b/db/joke/database_test.go @@ -41,7 +41,7 @@ func TestDatabaseService_Add(t *testing.T) { ctx := context.Background() - service := DatabaseService{test.NewMockedMongodbService(ctx, t.Client)} + service := Database{test.NewMockedMongodbService(ctx, t.Client)} if _, err := service.Add(ctx, testJoke); err != nil { mt.Fatal(err) @@ -53,7 +53,7 @@ func TestDatabaseService_AddWithContextCancelled(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Now()) cancel() - service := DatabaseService{test.NewMockedMongodbService(ctx, nil)} + service := Database{test.NewMockedMongodbService(ctx, nil)} if _, err := service.Add(ctx, testJoke); err == nil { t.Fatal("Context wasn't cancelled") @@ -64,7 +64,7 @@ func TestDatabaseService_JokeWithContextCancelled(t *testing.T) { ctx, cancel := context.WithDeadline(context.Background(), time.Now()) cancel() - service := DatabaseService{test.NewMockedMongodbService(ctx, nil)} + service := Database{test.NewMockedMongodbService(ctx, nil)} if _, err := service.Joke(ctx, testParams); err == nil { t.Fatal("Context wasn't cancelled") @@ -79,7 +79,7 @@ func TestDatabaseService_JokeReturnEmptyWithoutError(t *testing.T) { ctx := context.Background() - service := DatabaseService{test.NewMockedMongodbService(ctx, t.Client)} + service := Database{test.NewMockedMongodbService(ctx, t.Client)} if _, err := service.Joke(ctx, testParams); err == nil { mt.Fatal("Something was found in database, but it shouldn't") @@ -104,27 +104,27 @@ func TestDatabaseService_FindRandomJoke(t *testing.T) { ctx := context.Background() - service := DatabaseService{test.NewMockedMongodbService(ctx, t.Client)} + service := Database{test.NewMockedMongodbService(ctx, t.Client)} - joke, err := service.Joke(ctx, SearchParams{}) + res, err := service.Joke(ctx, SearchParams{}) if err != nil { t.Fatal(err) } - if joke.Category != testJoke.Category { - t.Fatalf("Invalid Category. Expected '%s', Result: '%s'", testJoke.Category, joke.Category) + if res.Category != testJoke.Category { + t.Fatalf("Invalid RawCategory. Expected '%s', Result: '%s'", testJoke.Category, res.Category) } - if joke.Type != testJoke.Type { - t.Fatalf("Invalid Type. Expected '%s', Result: '%s'", testJoke.Type, joke.Type) + if res.Type != testJoke.Type { + t.Fatalf("Invalid RawType. Expected '%s', Result: '%s'", testJoke.Type, res.Type) } - if joke.GuildID != testJoke.GuildID { - t.Fatalf("Invalid GuildID. Expected '%s', Result: '%s'", testJoke.GuildID, joke.GuildID) + if res.GuildID != testJoke.GuildID { + t.Fatalf("Invalid GuildID. Expected '%s', Result: '%s'", testJoke.GuildID, res.GuildID) } - if joke.Question != testJoke.Question { - t.Fatalf("Invalid Question. Expected '%s', Result: '%s'", testJoke.Question, joke.Question) + if res.Question != testJoke.Question { + t.Fatalf("Invalid Question. Expected '%s', Result: '%s'", testJoke.Question, res.Question) } - if joke.Answer != testJoke.Answer { - t.Fatalf("Invalid Answer. Expected '%s', Result: '%s'", testJoke.Answer, joke.Answer) + if res.Answer != testJoke.Answer { + t.Fatalf("Invalid Answer. Expected '%s', Result: '%s'", testJoke.Answer, res.Answer) } }) } @@ -135,7 +135,7 @@ func TestDatabaseService_ActiveWithContextCancelled(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) cancel() - service := DatabaseService{test.NewMockedMongodbService(ctx, t.Client)} + service := Database{test.NewMockedMongodbService(ctx, t.Client)} if service.Active(ctx) { t.Fatal("service can still running and handle new requests") @@ -151,36 +151,10 @@ func TestDatabaseService_Active(t *testing.T) { ctx := context.Background() - service := DatabaseService{test.NewMockedMongodbService(ctx, t.Client)} + service := Database{test.NewMockedMongodbService(ctx, t.Client)} if !service.Active(ctx) { t.Fatal("service isn't responding") } }) } - -func TestUnlockService(t *testing.T) { - testFlag := false - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() - resetTime := time.Now() - - unlockService(ctx, &testFlag, resetTime) - - if testFlag != true { - t.Fatal("Service doesn't unlock") - } -} - -func TestUnlockService_ParentContextCancelled(t *testing.T) { - testFlag := false - ctx, cancel := context.WithCancel(context.Background()) - resetTime := time.Now().Add(1 * time.Hour) - - cancel() - unlockService(ctx, &testFlag, resetTime) - - if testFlag != true { - t.Fatal("Service doesn't unlock") - } -} diff --git a/db/joke/types.go b/db/joke/types.go new file mode 100644 index 0000000..218e79f --- /dev/null +++ b/db/joke/types.go @@ -0,0 +1,140 @@ +package joke + +import ( + "context" + "errors" + komputer "github.com/wittano/komputer/api/proto" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AddService interface { + Add(ctx context.Context, joke Joke) (string, error) +} + +type SearchService interface { + // RandomJoke Joke Try to find Joke from Mongodb database. If SearchParams is empty, then function will find 1 random joke + RandomJoke(ctx context.Context, search SearchParams) (Joke, error) + ActiveChecker +} + +type ActiveChecker interface { + Active(ctx context.Context) bool +} + +type ( + Type string + Category string +) + +const ( + Single Type = "single" + TwoPart Type = "twopart" +) + +const ( + PROGRAMMING Category = "Programming" + MISC Category = "Misc" + DARK Category = "Dark" + YOMAMA Category = "YoMama" + Any Category = "Any" +) + +func (t Type) ApiType() (ty komputer.Type, err error) { + switch t { + case Single: + ty = komputer.Type_SINGLE + case TwoPart: + ty = komputer.Type_TWO_PART + default: + err = errors.New("joke: unknown type") + } + + return +} + +func (c Category) ApiCategory() (ca komputer.Category, err error) { + switch c { + case Any: + ca = komputer.Category_Any + case DARK: + ca = komputer.Category_DARK + case PROGRAMMING: + ca = komputer.Category_PROGRAMMING + case YOMAMA: + ca = komputer.Category_YOMAMA + case MISC: + ca = komputer.Category_MISC + default: + err = errors.New("joke: unknown category") + } + + return +} + +func RawType(api komputer.Type) (t Type, err error) { + switch api { + case komputer.Type_SINGLE: + t = Single + case komputer.Type_TWO_PART: + t = TwoPart + default: + err = errors.New("joke: unknown type") + } + + return +} + +func RawCategory(api komputer.Category) (c Category, err error) { + switch api { + case komputer.Category_Any: + c = Any + case komputer.Category_DARK: + c = DARK + case komputer.Category_PROGRAMMING: + c = PROGRAMMING + case komputer.Category_YOMAMA: + c = YOMAMA + case komputer.Category_MISC: + c = MISC + default: + err = errors.New("joke: unknown category") + } + + return +} + +type Joke struct { + ID primitive.ObjectID `bson:"_id"` + Question string `bson:"question"` + Answer string `bson:"answer"` + Type Type `bson:"type"` + Category Category `bson:"category"` + GuildID string `bson:"guild_id"` +} + +func (j Joke) ApiResponse() (*komputer.Joke, error) { + ty, err := j.Type.ApiType() + if err != nil { + return nil, err + } + + ca, err := j.Category.ApiCategory() + if err != nil { + return nil, err + } + + return &komputer.Joke{ + Id: &komputer.ObjectID{ObjectId: j.ID.Hex()}, + Answer: j.Answer, + Question: &j.Question, + Type: ty, + Category: ca, + GuildId: j.GuildID, + }, nil +} + +type SearchParams struct { + Type Type + Category Category + ID primitive.ObjectID +} diff --git a/db/mongo_test.go b/db/mongo_test.go index 8a023a2..717bbc3 100644 --- a/db/mongo_test.go +++ b/db/mongo_test.go @@ -6,7 +6,7 @@ import ( "testing" ) -const testUri = "mongodb+srv://username:password@server/database" +const testUri = "Mongodb+srv://username:password@server/database" func TestNewMongodbDatabase(t *testing.T) { os.Setenv(MongodbURIKey, testUri) @@ -28,7 +28,7 @@ func TestMongodbDatabase_ClientButURIMissing(t *testing.T) { db := Mongodb(ctx) if _, err := db.Client(ctx); err == nil { - t.Fatal("mongodb connection was established!") + t.Fatal("Mongodb connection was established!") } } @@ -39,6 +39,6 @@ func TestMongodbDatabase_ClientButConnectionFailed(t *testing.T) { db := Mongodb(ctx) if _, err := db.Client(ctx); err == nil { - t.Fatal("mongodb connection was established!") + t.Fatal("Mongodb connection was established!") } } diff --git a/go.work b/go.work index a3674b2..c65b45a 100644 --- a/go.work +++ b/go.work @@ -4,4 +4,5 @@ use ( . bot dgvoice + server ) diff --git a/go.work.sum b/go.work.sum index 94bab52..4d7fe53 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,6 +1,24 @@ +cloud.google.com/go/compute v1.25.1 h1:ZRpHJedLtTpKgr3RV1Fx23NuaAEN1Zfx9hw1u4aJdjU= +cloud.google.com/go/compute v1.25.1/go.mod h1:oopOIR53ly6viBYxaDhBfJwzUAxf1zE//uf3IB011ls= +cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= +cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +github.com/census-instrumentation/opencensus-proto v0.4.1 h1:iKLQ0xPNFxR/2hzXZMrBo8f1j86j5WHzznCCQxV/b8g= +github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50 h1:DBmgJDC9dTfkVyGgipamEh2BpGYxScCH1TOF1LL1cXc= +github.com/cncf/xds/go v0.0.0-20240318125728-8a4994d93e50/go.mod h1:5e1+Vvlzido69INQaVO6d87Qn543Xr6nooe9Kz7oBFM= +github.com/envoyproxy/go-control-plane v0.12.0 h1:4X+VP1GHd1Mhj6IB5mMeGbLCleqxjletLK6K0rbxyZI= +github.com/envoyproxy/go-control-plane v0.12.0/go.mod h1:ZBTaoJ23lqITozF0M6G4/IragXCQKCnYbmlmtHvwRG0= +github.com/envoyproxy/protoc-gen-validate v1.0.4 h1:gVPz/FMfvh57HdSJQyvBtF00j8JU4zdyUgIUNhlgg0A= +github.com/envoyproxy/protoc-gen-validate v1.0.4/go.mod h1:qys6tmnRsYrQqIhm2bvKZH4Blx/1gTIZ2UKVY1M+Yew= github.com/go-co-op/gocron v1.37.0/go.mod h1:3L/n6BkO7ABj+TrfSVXLRzsP26zmikL4ISkLQ0O8iNY= +github.com/golang/glog v1.2.0 h1:uCdmnmatrKCgMBlM4rMuJZWOkPDqdbZPnrMXDY4gI68= +github.com/golang/glog v1.2.0/go.mod h1:6AhwSGph0fcJtXVM/PEHPqZlFeoLxhs7/t5UDAwmO+w= github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= @@ -12,9 +30,15 @@ golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= +golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237 h1:RFiFrvy37/mpSpdySBDrUdipW/dHwsRwh3J3+A9VgT4= +google.golang.org/genproto/googleapis/api v0.0.0-20240318140521-94a12d6c2237/go.mod h1:Z5Iiy3jtmioajWHDGFk7CeugTyHtPvMHA4UTmUkyalE= diff --git a/proto/joke.proto b/proto/joke.proto index 894dc35..2053021 100644 --- a/proto/joke.proto +++ b/proto/joke.proto @@ -19,7 +19,7 @@ enum Category { } message Joke { - bytes id = 1; + komputer.ObjectID id = 1; string answer = 2; optional string question = 3; Type type = 4; @@ -34,12 +34,22 @@ message JokeID { } } -message DatabaseObjectID { - bytes object_id = 1; +message JokeParams { + optional komputer.ObjectID id = 1; + optional Category category = 2; + optional Type type = 3; +} + +message JokeParamsPagination { + optional komputer.ObjectID id = 1; + optional Category category = 2; + optional Type type = 3; + Pagination page = 4; } service JokeService { - rpc Find(komputer.FindById) returns (Joke); + rpc Find(JokeParams) returns (Joke); + rpc FindAll(JokeParamsPagination) returns (stream Joke); rpc Add(Joke) returns (JokeID); - rpc Remove(DatabaseObjectID) returns (google.protobuf.Empty); + rpc Remove(komputer.ObjectID) returns (google.protobuf.Empty); } \ No newline at end of file diff --git a/proto/request.proto b/proto/request.proto index 0c56b5d..f33551c 100644 --- a/proto/request.proto +++ b/proto/request.proto @@ -21,7 +21,7 @@ message UUID { } message ObjectID { - bytes object_id = 1; + string object_id = 1; } message NameOrIdRequest { diff --git a/server/go.mod b/server/go.mod new file mode 100644 index 0000000..69e10f4 --- /dev/null +++ b/server/go.mod @@ -0,0 +1,12 @@ +module server + +go 1.22 + +require ( + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 // indirect + google.golang.org/grpc v1.64.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect +) diff --git a/server/go.sum b/server/go.sum new file mode 100644 index 0000000..b52b26e --- /dev/null +++ b/server/go.sum @@ -0,0 +1,12 @@ +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5 h1:Q2RxlXqh1cgzzUgV261vBO2jI5R/3DD1J2pM0nI4NhU= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240520151616-dc85e6b867a5/go.mod h1:EfXuqaE1J41VCDicxHzUDm+8rk+7ZdXzHV0IhO/I6s0= +google.golang.org/grpc v1.64.0 h1:KH3VH9y/MgNQg1dE7b3XfVK0GsPSIzJwdF617gUSbvY= +google.golang.org/grpc v1.64.0/go.mod h1:oxjF8E3FBnjp+/gVFYdWacaLDx9na1aqy9oovLpxQYg= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= diff --git a/server/joke.go b/server/joke.go new file mode 100644 index 0000000..afda275 --- /dev/null +++ b/server/joke.go @@ -0,0 +1,116 @@ +package server + +import ( + "context" + "errors" + komputer "github.com/wittano/komputer/api/proto" + "github.com/wittano/komputer/db/joke" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/protobuf/types/known/emptypb" +) + +type jokeServer struct { + Db joke.Database + + komputer.UnimplementedJokeServiceServer +} + +type apiJokeParams interface { + GetId() *komputer.ObjectID + GetCategory() komputer.Category + GetType() komputer.Type +} + +func (j jokeServer) Find(ctx context.Context, params *komputer.JokeParams) (*komputer.Joke, error) { + p, err := searchParams(params) + if err != nil { + return nil, err + } + + entity, err := j.Db.Jokes(ctx, p, nil) + if err != nil { + return nil, err + } + + return entity[0].ApiResponse() +} + +func (j jokeServer) FindAll(identity *komputer.JokeParamsPagination, server komputer.JokeService_FindAllServer) error { + p, err := searchParams(identity) + if err != nil { + return err + } + + page := identity.Page + if page == nil { + page = &komputer.Pagination{Size: 10} + } + + jokes, err := j.Db.Jokes(server.Context(), p, identity.Page) + if err != nil { + return err + } + + for _, jokeDb := range jokes { + response, err := jokeDb.ApiResponse() + if err != nil { + continue + } + + err = errors.Join(err, server.Send(response)) + } + + return err +} + +func (j jokeServer) Remove(ctx context.Context, id *komputer.ObjectID) (*emptypb.Empty, error) { + return &emptypb.Empty{}, j.Db.Remove(ctx, id.ObjectId) +} + +func (j jokeServer) Add(ctx context.Context, joke *komputer.Joke) (*komputer.JokeID, error) { + newJoke, err := newJoke(joke) + if err != nil { + return nil, err + } + + id, err := j.Db.Add(ctx, newJoke) + if err != nil { + return nil, err + } + + return &komputer.JokeID{Id: &komputer.JokeID_ObjectId{ObjectId: []byte(id)}}, err +} + +func newJoke(j *komputer.Joke) (new joke.Joke, err error) { + if j == nil { + err = errors.New("missing joke data") + return + } + + new.Category, err = joke.RawCategory(j.Category) + new.Type, err = joke.RawType(j.Type) + new.Answer = j.Answer + new.GuildID = j.GuildId + + if j.Question != nil { + new.Question = *j.Question + } + + return +} + +func searchParams(params apiJokeParams) (p joke.SearchParams, err error) { + if params == nil { + err = errors.New("missing joke params") + return + } + + if params.GetId() != nil { + p.ID, err = primitive.ObjectIDFromHex(params.GetId().ObjectId) + } + + p.Type, err = joke.RawType(params.GetType()) + p.Category, err = joke.RawCategory(params.GetCategory()) + + return +} diff --git a/server/server.go b/server/server.go new file mode 100644 index 0000000..2403c82 --- /dev/null +++ b/server/server.go @@ -0,0 +1,48 @@ +package server + +import ( + "context" + komputer "github.com/wittano/komputer/api/proto" + "github.com/wittano/komputer/db" + "github.com/wittano/komputer/db/joke" + "google.golang.org/grpc" + "log" + "net" + "strconv" +) + +type Server struct { + l net.Listener + serv *grpc.Server +} + +func (s Server) Start() error { + log.Printf("Server listing on port %serv\n", s.l.Addr()) + + return s.serv.Serve(s.l) +} + +func (s Server) Close() error { + s.serv.Stop() + return s.l.Close() +} + +func New(port uint64) (Server, error) { + l, err := net.Listen("tcp", strconv.FormatUint(port, 10)) + if err != nil { + return Server{}, err + } + + return Server{l, newGRPGServer()}, nil +} + +func newGRPGServer() *grpc.Server { + s := grpc.NewServer() + + ctx := context.Background() + mongodb := db.Mongodb(ctx) + + komputer.RegisterJokeServiceServer(s, &jokeServer{Db: joke.Database{Mongodb: mongodb}}) + + return s +}