├── Makefile ├── .gitignore ├── internal ├── storage │ ├── pg │ │ ├── metrics.go │ │ ├── new.go │ │ ├── user.go │ │ └── word.go │ ├── new.go │ └── sqlite │ │ ├── metrics.go │ │ ├── new.go │ │ ├── migration.go │ │ ├── user.go │ │ └── word.go ├── entities │ ├── log.go │ ├── language.go │ ├── user.go │ └── word.go ├── service │ ├── ask │ │ ├── prompt.go │ │ ├── ask_test.go │ │ ├── prompt-texts.go │ │ └── main.go │ ├── new.go │ ├── metrics │ │ └── new.go │ ├── play │ │ ├── play_test.go │ │ └── main.go │ ├── word │ │ └── main.go │ └── user │ │ └── main.go └── transport │ └── rest │ ├── handler │ ├── metric.go │ ├── play.go │ ├── new.go │ ├── ask.go │ ├── common.go │ ├── user.go │ └── word.go │ ├── new.go │ └── middleware │ └── middleware.go ├── Dockerfile ├── cmd └── main.go ├── go.mod ├── config └── load.go └── go.sum /Makefile: -------------------------------------------------------------------------------- 1 | run: build 2 | ./build/yerd 3 | 4 | build: clean 5 | go build -o ./build/yerd cmd/main.go 6 | 7 | clean: 8 | rm -f ./build/yerd -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .TODO 3 | 4 | 5 | 6 | 7 | #db files 8 | 9 | .db 10 | log.db 11 | main.db 12 | 13 | build/word 14 | 15 | ./build/word 16 | 17 | /build/word 18 | 19 | build/ -------------------------------------------------------------------------------- /internal/storage/pg/metrics.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | func (repo *Repository) AddLog(Type, Data, Time string) error { 4 | query := ` 5 | INSERT INTO logs (type, data, created_at) 6 | VALUES ($1, $2, $3) 7 | ` 8 | _, err := repo.Exec(query, Type, Data, Time) 9 | return err 10 | } 11 | -------------------------------------------------------------------------------- /internal/entities/log.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import "time" 4 | 5 | type Log struct { 6 | ID int `json:"id" db:"id"` 7 | Type string `json:"type" db:"type"` 8 | Data string `json:"data" db:"data"` 9 | CreatedAt time.Time `json:"created_at" db:"created_at"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/entities/language.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type Language struct { 8 | ID string `db:"id" json:"id"` 9 | UserID string `db:"user_id" json:"user_id"` 10 | LanguageName string `db:"name" json:"name"` 11 | CreatedAt time.Time `db:"created_at" json:"created_at"` 12 | } 13 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22 AS builder-backend 2 | 3 | WORKDIR /build 4 | 5 | COPY go.mod go.sum ./ 6 | RUN go mod download 7 | 8 | COPY ./ ./ 9 | RUN CGO_ENABLED=1 go build -o /build/yerd cmd/main.go 10 | 11 | FROM gcr.io/distroless/base-debian12 AS backend 12 | 13 | WORKDIR /app 14 | COPY --from=builder-backend /build/yerd . 15 | COPY .env /app/.env 16 | 17 | CMD ["./yerd"] -------------------------------------------------------------------------------- /internal/storage/new.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "word/internal/storage/sqlite" 5 | ) 6 | 7 | type Storage struct { 8 | DB *sqlite.Repository 9 | } 10 | 11 | func New() (*Storage, error) { 12 | db, err := sqlite.New() 13 | if err != nil { 14 | return &Storage{}, err 15 | } 16 | return &Storage{ 17 | DB: db, 18 | }, nil 19 | } 20 | 21 | func (repo *Storage) CloseConnections() { 22 | repo.DB.Close() 23 | } 24 | -------------------------------------------------------------------------------- /internal/entities/user.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type User struct { 8 | ID string `db:"id" json:"id"` 9 | Name string `db:"name" json:"name"` 10 | FullName string `db:"full_name" json:"full_name"` 11 | Email string `db:"email" json:"email"` 12 | Avatar string `db:"avatar" json:"avatar"` 13 | Language string `db:"language" json:"language"` 14 | CreatedAt time.Time `db:"created_at" json:"created_at"` 15 | } 16 | -------------------------------------------------------------------------------- /internal/service/ask/prompt.go: -------------------------------------------------------------------------------- 1 | package ask 2 | 3 | import ( 4 | "regexp" 5 | ) 6 | 7 | // promptGenarate is entry point 8 | func promptGenarate(UserLanguage string, TargetLanguage string, Word string) string { 9 | return putWord(getPropmtWithLanguage(UserLanguage, TargetLanguage), Word) 10 | } 11 | 12 | // putWord puts the word to prompt 13 | func putWord(Text string, Word string) string { 14 | regex := regexp.MustCompile(`\[\[.*?\]\]`) 15 | return regex.ReplaceAllString(Text, Word) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "word/config" 6 | "word/internal/service" 7 | "word/internal/storage" 8 | "word/internal/transport/rest" 9 | ) 10 | 11 | func main() { 12 | //Load config 13 | config.Load() 14 | 15 | //Connect DB 16 | store, err := storage.New() 17 | if err != nil { 18 | log.Println(err.Error()) 19 | panic("Panic: storage is not created") 20 | } 21 | defer store.CloseConnections() 22 | 23 | //Define services 24 | app := service.New(store.DB) 25 | 26 | //Run the server 27 | server := rest.New(app) 28 | server.Serve() 29 | } 30 | -------------------------------------------------------------------------------- /internal/storage/pg/new.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "word/config" 5 | 6 | "github.com/jmoiron/sqlx" 7 | _ "github.com/lib/pq" 8 | ) 9 | 10 | type Repository struct { 11 | *sqlx.DB 12 | } 13 | 14 | func New() (*Repository, error) { 15 | db, err := sqlx.Open("postgres", config.PgConnStr) 16 | if err != nil { 17 | return &Repository{}, err 18 | } 19 | 20 | err = db.Ping() 21 | if err != nil { 22 | return &Repository{}, err 23 | } 24 | return &Repository{ 25 | DB: db, 26 | }, nil 27 | } 28 | 29 | func (repo *Repository) Close() { 30 | repo.DB.Close() 31 | } 32 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/metric.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "word/config" 7 | ) 8 | 9 | func (h *Handler) Visits(w http.ResponseWriter, r *http.Request) { 10 | Login := r.FormValue("login") 11 | Pass := r.FormValue("pass") 12 | if Login != config.AdminLogin || Pass != config.AdminPass { 13 | http.Error(w, "Wrong login or password", http.StatusUnauthorized) 14 | return 15 | } 16 | visitLogs, err := h.s.Metrics.VisitLogs() 17 | if err != nil { 18 | http.Error(w, err.Error(), http.StatusBadRequest) 19 | return 20 | } 21 | 22 | w.WriteHeader(http.StatusOK) 23 | json.NewEncoder(w).Encode(visitLogs) 24 | } 25 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module word 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 // direct 7 | github.com/joho/godotenv v1.5.1 // direct 8 | golang.org/x/oauth2 v0.21.0 // direct 9 | ) 10 | 11 | require ( 12 | cloud.google.com/go/compute/metadata v0.3.0 // indirect 13 | github.com/Masterminds/squirrel v1.5.4 // indirect 14 | github.com/golang-jwt/jwt/v5 v5.2.1 // indirect 15 | github.com/jmoiron/sqlx v1.4.0 // indirect 16 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect 17 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect 18 | github.com/lib/pq v1.10.9 // indirect 19 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 20 | github.com/sashabaranov/go-openai v1.26.0 // indirect 21 | ) 22 | -------------------------------------------------------------------------------- /internal/service/new.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "word/internal/service/ask" 5 | "word/internal/service/metrics" 6 | "word/internal/service/play" 7 | "word/internal/service/user" 8 | "word/internal/service/word" 9 | ) 10 | 11 | type DB interface { 12 | ask.DB 13 | user.DB 14 | word.DB 15 | play.DB 16 | metrics.DB 17 | } 18 | 19 | type Service struct { 20 | Ask *ask.AskService 21 | User *user.UserService 22 | Word *word.WordService 23 | Play *play.PlayService 24 | Metrics *metrics.MetricsService 25 | } 26 | 27 | func New(db DB) *Service { 28 | return &Service{ 29 | Ask: ask.New(db), 30 | User: user.New(db), 31 | Word: word.New(db), 32 | Play: play.New(db), 33 | Metrics: metrics.New(db), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /internal/entities/word.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type WordBasic struct { 8 | ID string `db:"id" json:"id"` 9 | Title string `db:"title" json:"title"` 10 | Description string `db:"description" json:"description"` 11 | FromLanguage string `db:"from_language" json:"from_language"` 12 | ToLanguage string `db:"to_language" json:"to_language"` 13 | Type string `db:"type" json:"type"` 14 | } 15 | 16 | type Word struct { 17 | WordBasic 18 | CreatedAt time.Time `db:"created_at" json:"created_at"` 19 | UpdatedAt time.Time `db:"updated_at" json:"updated_at"` 20 | UserID string `db:"user_id" json:"user_id"` 21 | } 22 | 23 | type WordsWithLanguage struct { 24 | Language string `json:"language"` 25 | Words []Word `json:"words"` 26 | } 27 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/play.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "strconv" 7 | ) 8 | 9 | func (h *Handler) Play(w http.ResponseWriter, r *http.Request) { 10 | UserID, _, err := CheckAuth(r) 11 | if err != nil { 12 | http.Error(w, err.Error(), http.StatusBadRequest) 13 | return 14 | } 15 | countValue := r.FormValue("count") 16 | count, err := strconv.ParseInt(countValue, 10, 8) 17 | if err != nil { 18 | http.Error(w, "count must be number", http.StatusBadRequest) 19 | return 20 | } 21 | langValue := r.FormValue("lang") 22 | words, err := h.s.Play.GeneratePlay(UserID, int(count), langValue) 23 | res := map[string]interface{}{ 24 | "count": len(words), 25 | "words": words, 26 | } 27 | w.WriteHeader(http.StatusOK) 28 | json.NewEncoder(w).Encode(res) 29 | } 30 | -------------------------------------------------------------------------------- /internal/storage/sqlite/metrics.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "time" 5 | "word/internal/entities" 6 | 7 | sq "github.com/Masterminds/squirrel" 8 | ) 9 | 10 | func (repo *Repository) AddLog(Type, Data string, Time time.Time) error { 11 | sql, agrs, err := sq.Insert("logs").Columns("type", "data", "created_at").Values(Type, Data, Time).ToSql() 12 | if err != nil { 13 | return err 14 | } 15 | _, err = repo.logdb.Exec(sql, agrs...) 16 | return err 17 | } 18 | 19 | func (repo *Repository) GetLogs(Type string) ([]entities.Log, error) { 20 | query, args, err := sq.Select("id", "type", "data", "created_at"). 21 | From("logs").Where(sq.Eq{"type": Type}).OrderBy("created_at DESC").ToSql() 22 | if err != nil { 23 | return []entities.Log{}, nil 24 | } 25 | logs := make([]entities.Log, 0, 0) 26 | err = repo.logdb.Select(&logs, query, args...) 27 | if err != nil { 28 | return []entities.Log{}, err 29 | } 30 | return logs, nil 31 | 32 | } 33 | -------------------------------------------------------------------------------- /internal/storage/sqlite/new.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "word/config" 5 | 6 | "github.com/jmoiron/sqlx" 7 | _ "github.com/mattn/go-sqlite3" 8 | ) 9 | 10 | type Repository struct { 11 | db *sqlx.DB 12 | logdb *sqlx.DB 13 | } 14 | 15 | func New() (*Repository, error) { 16 | db, err := sqlx.Open("sqlite3", config.SQLiteMainPath) 17 | if err != nil { 18 | return &Repository{}, err 19 | } 20 | 21 | err = db.Ping() 22 | if err != nil { 23 | return &Repository{}, err 24 | } 25 | logdb, err := sqlx.Open("sqlite3", config.SQLiteLogPath) 26 | if err != nil { 27 | return &Repository{}, err 28 | } 29 | 30 | err = logdb.Ping() 31 | if err != nil { 32 | return &Repository{}, err 33 | } 34 | 35 | newDB := &Repository{ 36 | db: db, 37 | logdb: logdb, 38 | } 39 | err = newDB.runMigration() 40 | if err != nil { 41 | return &Repository{}, err 42 | } 43 | return newDB, nil 44 | } 45 | 46 | func (repo *Repository) Close() { 47 | repo.db.Close() 48 | repo.logdb.Close() 49 | } 50 | -------------------------------------------------------------------------------- /internal/service/metrics/new.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "time" 8 | "word/internal/entities" 9 | ) 10 | 11 | type DB interface { 12 | //AddLog should get Type of log, Data of log and Time of log, and returns error 13 | AddLog(string, string, time.Time) error 14 | GetLogs(string) ([]entities.Log, error) 15 | } 16 | 17 | type MetricsService struct { 18 | db DB 19 | } 20 | 21 | func New(db DB) *MetricsService { 22 | return &MetricsService{db} 23 | } 24 | 25 | func (m MetricsService) Visit(Status int, Path, Method, Duration string) error { 26 | data := map[string]string{ 27 | "status": fmt.Sprint(Status), 28 | "path": Path, 29 | "method": Method, 30 | "duration": Duration, 31 | } 32 | buf, err := json.Marshal(data) 33 | if err != nil { 34 | return errors.New("Json marshaling failed") 35 | } 36 | return m.db.AddLog("visit", string(buf), time.Now()) 37 | } 38 | 39 | func (m MetricsService) VisitLogs() ([]entities.Log, error) { 40 | return m.db.GetLogs("visit") 41 | } 42 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/new.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "word/internal/service" 6 | "word/internal/transport/rest/middleware" 7 | ) 8 | 9 | type Handler struct { 10 | s *service.Service 11 | } 12 | 13 | func New(s *service.Service) *Handler { 14 | return &Handler{s: s} 15 | } 16 | 17 | func (h *Handler) Handle() *http.ServeMux { 18 | //Define the main route 19 | handlers := http.NewServeMux() 20 | middl := middleware.NewLogger(h.s.Metrics) 21 | 22 | //Define the api routes 23 | apiv1 := http.NewServeMux() 24 | apiv1.HandleFunc("POST /word", middl(h.CreateWord)) 25 | apiv1.HandleFunc("PATCH /word", middl(h.UpdateWord)) 26 | apiv1.HandleFunc("DELETE /word/{id}", middl(h.DeleteWord)) 27 | apiv1.HandleFunc("/word/{id}", middl(h.Word)) 28 | apiv1.HandleFunc("/word", middl(h.Words)) 29 | apiv1.HandleFunc("PATCH /onboard", middl(h.OnboardUpdate)) 30 | apiv1.HandleFunc("/play", middl(h.Play)) 31 | apiv1.HandleFunc("POST /ask", h.Ask) 32 | apiv1.HandleFunc("/me", middl(h.Me)) 33 | apiv1.HandleFunc("/metrics/visits", h.Visits) 34 | handlers.Handle("/api/v1/", http.StripPrefix("/api/v1", apiv1)) 35 | 36 | //Define oauth2 routes 37 | oauth2_google := http.NewServeMux() 38 | oauth2_google.HandleFunc("/login", middl(h.GoogleLoginURL)) 39 | oauth2_google.HandleFunc("/callback", middl(h.GoogleCallback)) 40 | 41 | handlers.Handle("/oauth/google/", http.StripPrefix("/oauth/google", oauth2_google)) 42 | 43 | return handlers 44 | } 45 | -------------------------------------------------------------------------------- /config/load.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/joho/godotenv" 8 | "golang.org/x/oauth2" 9 | "golang.org/x/oauth2/google" 10 | ) 11 | 12 | var ( 13 | Mode string 14 | Port string 15 | OAuthConfig oauth2.Config 16 | OAuthState string 17 | PgConnStr string 18 | JwtKey string 19 | OpenaiToken string 20 | RedirectUser string 21 | SQLiteMainPath string 22 | SQLiteLogPath string 23 | AdminLogin string 24 | AdminPass string 25 | ) 26 | 27 | func Load() { 28 | err := godotenv.Load() 29 | if err != nil { 30 | log.Fatal("Error loading .env file") 31 | } 32 | Mode = os.Getenv("MODE") 33 | Port = os.Getenv("PORT") 34 | OAuthConfig = oauth2.Config{ 35 | ClientID: os.Getenv("GOOGLEID"), 36 | ClientSecret: os.Getenv("GOOGLESECRET"), 37 | RedirectURL: os.Getenv("GOOGLEREDIRECT"), 38 | Scopes: []string{"profile", "email", "https://www.googleapis.com/auth/userinfo.profile", "https://www.googleapis.com/auth/userinfo.email"}, 39 | Endpoint: google.Endpoint, 40 | } 41 | OAuthState = os.Getenv("OAUTH_STATE") 42 | PgConnStr = os.Getenv("PG_CONN_STR") 43 | JwtKey = os.Getenv("JWT_KEY") 44 | OpenaiToken = os.Getenv("OPENAI_TOKEN") 45 | RedirectUser = os.Getenv("REDIRECT_USER") 46 | SQLiteMainPath = os.Getenv("SQLITE_MAIN") 47 | SQLiteLogPath = os.Getenv("SQLITE_LOG") 48 | AdminLogin = os.Getenv("ADMIN_LOGIN") 49 | AdminPass = os.Getenv("ADMIN_PASSWORD") 50 | } 51 | -------------------------------------------------------------------------------- /internal/storage/pg/user.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import "word/internal/entities" 4 | 5 | func (repo *Repository) CreateUser(User entities.User) error { 6 | query := ` 7 | INSERT INTO users (id, name, full_name, email, avatar, language) 8 | VALUES ($1, $2, $3, $4, $5, $6) 9 | ` 10 | _, err := repo.Exec(query, User.ID, User.Name, User.FullName, User.Email, User.Avatar, User.Language) 11 | return err 12 | } 13 | 14 | func (repo *Repository) UserByEmail(Email string) (entities.User, error) { 15 | query := "SELECT id, name, full_name, email, avatar, language, created_at FROM users WHERE email = $1" 16 | var user entities.User 17 | err := repo.Get(&user, query, Email) 18 | return user, err 19 | } 20 | 21 | func (repo *Repository) Languages(UserID string) ([]entities.Language, error) { 22 | query := "SELECT id, user_id, name, created_at FROM languages WHERE user_id = $1" 23 | languages := make([]entities.Language, 0) 24 | err := repo.Select(&languages, query, UserID) 25 | return languages, err 26 | } 27 | 28 | func (repo *Repository) UpdateUserLanguage(UserLanguage string, UserID string) error { 29 | query := `UPDATE users SET language = $2 WHERE id = $1` 30 | _, err := repo.Exec(query, UserID, UserLanguage) 31 | return err 32 | } 33 | 34 | func (repo *Repository) CreateLanguages(Languages []entities.Language) error { 35 | query := `INSERT INTO languages (id, user_id, name, created_at) 36 | VALUES (:id, :user_id, :name, :created_at)` 37 | _, err := repo.NamedExec(query, Languages) 38 | return err 39 | } 40 | -------------------------------------------------------------------------------- /internal/transport/rest/new.go: -------------------------------------------------------------------------------- 1 | package rest 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "os/signal" 9 | "syscall" 10 | "time" 11 | "word/config" 12 | "word/internal/service" 13 | "word/internal/transport/rest/handler" 14 | ) 15 | 16 | type RestServer struct { 17 | services *service.Service 18 | } 19 | 20 | func New(services *service.Service) *RestServer { 21 | return &RestServer{ 22 | services: services, 23 | } 24 | } 25 | 26 | func (s *RestServer) Serve() { 27 | // Context for the server 28 | ctx, cancel := context.WithCancel(context.Background()) 29 | defer cancel() 30 | 31 | // Signal to close app 32 | osSignal := make(chan os.Signal, 1) 33 | signal.Notify(osSignal, os.Interrupt, syscall.SIGTERM) 34 | 35 | handlers := handler.New(s.services).Handle() 36 | server := &http.Server{ 37 | Addr: config.Port, 38 | Handler: handlers, 39 | } 40 | 41 | go func() { 42 | log.Printf("Starting server on port %s", config.Port) 43 | if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { 44 | log.Fatalf("Server failed: %v", err) 45 | } 46 | }() 47 | 48 | select { 49 | case <-osSignal: 50 | log.Println("Shutdown signal received. Shutting down...") 51 | 52 | // Timeout to close handlers 53 | shutdownCtx, shutdownCancel := context.WithTimeout(ctx, 30*time.Second) 54 | defer shutdownCancel() 55 | 56 | // Stop the server 57 | if err := server.Shutdown(shutdownCtx); err != nil { 58 | log.Fatalf("Server shutdown failed: %v", err) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /internal/transport/rest/middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "net/http" 6 | "time" 7 | ) 8 | 9 | type Storage interface { 10 | Visit(int, string, string, string) error 11 | } 12 | 13 | func NewLogger(s Storage) func(func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 14 | return func(handler func(http.ResponseWriter, *http.Request)) http.HandlerFunc { 15 | return with(s, handler, info) 16 | } 17 | } 18 | 19 | type responseWriter struct { 20 | http.ResponseWriter 21 | status int 22 | } 23 | 24 | func (rw *responseWriter) WriteHeader(statusCode int) { 25 | rw.status = statusCode 26 | rw.ResponseWriter.WriteHeader(statusCode) 27 | } 28 | 29 | func (rw *responseWriter) Status() int { 30 | if rw.status == 0 { 31 | return http.StatusOK 32 | } 33 | return rw.status 34 | } 35 | 36 | func info(next http.HandlerFunc, s Storage) http.HandlerFunc { 37 | hf := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 38 | 39 | start := time.Now() 40 | wrapped := &responseWriter{ResponseWriter: w} 41 | next.ServeHTTP(wrapped, r) 42 | 43 | log.Printf("Status:%v Path: %s %s, in %v", wrapped.status, r.Method, r.URL.Path, time.Since(start)) 44 | s.Visit(wrapped.status, r.URL.Path, r.Method, time.Since(start).String()) 45 | }) 46 | return hf 47 | } 48 | 49 | func with(s Storage, handler func(http.ResponseWriter, *http.Request), middlewares ...func(http.HandlerFunc, Storage) http.HandlerFunc) http.HandlerFunc { 50 | finalHandler := http.HandlerFunc(handler) 51 | for _, middleware := range middlewares { 52 | finalHandler = middleware(finalHandler, s) 53 | } 54 | return finalHandler 55 | } 56 | -------------------------------------------------------------------------------- /internal/storage/sqlite/migration.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | func (repo *Repository) runMigration() error { 4 | _, err := repo.db.Exec(` 5 | CREATE TABLE IF NOT EXISTS users ( 6 | id TEXT PRIMARY KEY, 7 | name TEXT NOT NULL, 8 | full_name TEXT, 9 | email TEXT NOT NULL UNIQUE, 10 | avatar TEXT, 11 | language TEXT, 12 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 13 | role TEXT 14 | ) 15 | `) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | // Создание таблицы languages 21 | _, err = repo.db.Exec(` 22 | CREATE TABLE IF NOT EXISTS languages ( 23 | id TEXT PRIMARY KEY, 24 | user_id TEXT, 25 | name TEXT NOT NULL, 26 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 27 | FOREIGN KEY(user_id) REFERENCES users(id) 28 | ) 29 | `) 30 | if err != nil { 31 | return err 32 | } 33 | 34 | // Создание таблицы words 35 | _, err = repo.db.Exec(` 36 | CREATE TABLE IF NOT EXISTS words ( 37 | id TEXT PRIMARY KEY, 38 | title TEXT NOT NULL, 39 | description TEXT, 40 | from_language TEXT, 41 | to_language TEXT, 42 | type TEXT, 43 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 44 | updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 45 | user_id TEXT, 46 | is_deleted INTEGER DEFAULT 0, 47 | FOREIGN KEY(user_id) REFERENCES users(id) 48 | ) 49 | `) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | // Создание таблицы logs 55 | _, err = repo.logdb.Exec(` 56 | CREATE TABLE IF NOT EXISTS logs ( 57 | id INTEGER PRIMARY KEY AUTOINCREMENT, 58 | type TEXT, 59 | data TEXT, 60 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 61 | ) 62 | `) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/ask.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | ) 7 | 8 | type askBody struct { 9 | ID string `json:"id"` 10 | Oslang string `json:"oslang"` 11 | Tolang string `json:"tolang"` 12 | Word string `json:"word"` 13 | } 14 | 15 | type FlushWriter struct { 16 | flush http.Flusher 17 | wHttp http.ResponseWriter 18 | } 19 | 20 | func NewWriter(flush http.Flusher, wHttp http.ResponseWriter) *FlushWriter { 21 | return &FlushWriter{ 22 | flush: flush, 23 | wHttp: wHttp, 24 | } 25 | } 26 | 27 | func (w *FlushWriter) Write(p []byte) (int, error) { 28 | n, err := w.wHttp.Write(p) 29 | w.flush.Flush() 30 | return n, err 31 | } 32 | 33 | func (h *Handler) Ask(w http.ResponseWriter, r *http.Request) { 34 | UserID, _, err := CheckAuth(r) 35 | if err != nil { 36 | http.Error(w, err.Error(), http.StatusBadRequest) 37 | return 38 | } 39 | // Get data from body 40 | var askparams askBody 41 | decoder := json.NewDecoder(r.Body) 42 | defer r.Body.Close() 43 | err = decoder.Decode(&askparams) 44 | if err != nil { 45 | http.Error(w, "Incorrect body", http.StatusBadRequest) 46 | return 47 | } 48 | 49 | //Set headers for streaming 50 | w.Header().Set("Content-Type", "text/event-stream; charset=utf-8") 51 | w.Header().Set("Cache-Control", "no-cache") 52 | w.Header().Set("Connection", "keep-alive") 53 | 54 | //Create flusher 55 | flusher, ok := w.(http.Flusher) 56 | if !ok { 57 | http.Error(w, "Error on creating flusher", http.StatusBadRequest) 58 | return 59 | } 60 | 61 | //Create Writer function 62 | Writer := NewWriter(flusher, w) 63 | 64 | err = h.s.Ask.GenerateWord(askparams.ID, UserID, askparams.Oslang, askparams.Tolang, askparams.Word, Writer) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusBadRequest) 67 | } 68 | w.WriteHeader(http.StatusCreated) 69 | } 70 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/common.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | "word/config" 8 | 9 | "github.com/golang-jwt/jwt/v5" 10 | ) 11 | 12 | // JWT GENERATION START 13 | type MyClaims struct { 14 | UserID string `json:"user_id"` 15 | UserEmail string `json:"user_email"` 16 | jwt.RegisteredClaims 17 | } 18 | 19 | var jwtKey []byte 20 | 21 | func init() { 22 | jwtKey = []byte(config.JwtKey) 23 | } 24 | 25 | func CreateJWT(userID string, userEmail string) (string, error) { 26 | expirationTime := time.Now().Add(24 * 60 * time.Hour) 27 | 28 | claims := &MyClaims{ 29 | UserID: userID, 30 | UserEmail: userEmail, 31 | RegisteredClaims: jwt.RegisteredClaims{ 32 | ExpiresAt: jwt.NewNumericDate(expirationTime), 33 | }, 34 | } 35 | 36 | token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) 37 | 38 | tokenString, err := token.SignedString(jwtKey) 39 | if err != nil { 40 | return "", err 41 | } 42 | 43 | return tokenString, nil 44 | } 45 | 46 | func VerifyJWT(tokenString string) (*MyClaims, error) { 47 | claims := &MyClaims{} 48 | 49 | token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) { 50 | return jwtKey, nil 51 | }) 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if !token.Valid { 57 | return nil, fmt.Errorf("invalid token") 58 | } 59 | 60 | return claims, nil 61 | } 62 | 63 | //JWT FUNCS END 64 | 65 | // CHECK AUTH START 66 | func CheckAuth(r *http.Request) (string, string, error) { 67 | token := r.Header.Get("Authorization") 68 | claims, err := VerifyJWT(token) 69 | if err != nil { 70 | fmt.Println(err.Error()) 71 | return "", "", err 72 | } 73 | return claims.UserID, claims.UserEmail, nil 74 | } 75 | 76 | //CHECK AUTH END 77 | 78 | // GET COOKIE 79 | func createCookie(exp time.Time, key string, value string) http.Cookie { 80 | return http.Cookie{ 81 | Name: key, 82 | Value: value, 83 | Expires: exp, 84 | HttpOnly: false, 85 | Path: "/", 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /internal/storage/sqlite/user.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "word/internal/entities" 5 | 6 | sq "github.com/Masterminds/squirrel" 7 | ) 8 | 9 | func (repo *Repository) CreateUser(User entities.User) error { 10 | query, args, err := sq.Insert("users"). 11 | Columns("id", "name", "full_name", "email", "avatar", "language"). 12 | Values(User.ID, User.Name, User.FullName, User.Email, User.Avatar, User.Language). 13 | ToSql() 14 | if err != nil { 15 | return err 16 | } 17 | _, err = repo.db.Exec(query, args...) 18 | return err 19 | } 20 | 21 | func (repo *Repository) UserByEmail(email string) (entities.User, error) { 22 | query, args, err := sq.Select("id", "name", "full_name", "email", "avatar", "language", "created_at"). 23 | From("users"). 24 | Where(sq.Eq{"email": email}). 25 | ToSql() 26 | if err != nil { 27 | return entities.User{}, err 28 | } 29 | 30 | var user entities.User 31 | err = repo.db.Get(&user, query, args...) 32 | return user, err 33 | } 34 | 35 | func (repo *Repository) Languages(userID string) ([]entities.Language, error) { 36 | query, args, err := sq.Select("id", "user_id", "name", "created_at"). 37 | From("languages"). 38 | Where(sq.Eq{"user_id": userID}). 39 | ToSql() 40 | if err != nil { 41 | return nil, err 42 | } 43 | 44 | languages := make([]entities.Language, 0) 45 | err = repo.db.Select(&languages, query, args...) 46 | return languages, err 47 | } 48 | 49 | func (repo *Repository) UpdateUserLanguage(userLanguage string, userID string) error { 50 | query, args, err := sq.Update("users"). 51 | Set("language", userLanguage). 52 | Where(sq.Eq{"id": userID}). 53 | ToSql() 54 | if err != nil { 55 | return err 56 | } 57 | 58 | _, err = repo.db.Exec(query, args...) 59 | return err 60 | } 61 | 62 | func (repo *Repository) CreateLanguages(languages []entities.Language) error { 63 | insertQuery := sq.Insert("languages"). 64 | Columns("id", "user_id", "name", "created_at") 65 | for _, language := range languages { 66 | insertQuery = insertQuery.Values(language.ID, language.UserID, language.LanguageName, language.CreatedAt) 67 | } 68 | query, args, err := insertQuery.ToSql() 69 | if err != nil { 70 | return err 71 | } 72 | _, err = repo.db.Exec(query, args...) 73 | return err 74 | } 75 | -------------------------------------------------------------------------------- /internal/storage/pg/word.go: -------------------------------------------------------------------------------- 1 | package pg 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | "word/internal/entities" 7 | ) 8 | 9 | func (repo *Repository) CreateWord(Word entities.Word) error { 10 | query := ` 11 | INSERT INTO words (id, title, description, from_language, to_language, type, created_at, updated_at, user_id) 12 | VALUES (:id, :title, :description, :from_language, :to_language, :type, :created_at, :updated_at, :user_id) 13 | ` 14 | _, err := repo.NamedExec(query, Word) 15 | return err 16 | } 17 | 18 | func (repo *Repository) Word(ID string) (entities.Word, error) { 19 | query := ` 20 | SELECT id, title, description, from_language, to_language, type, created_at, updated_at, user_id 21 | FROM words 22 | WHERE id = $1 23 | ` 24 | var word entities.Word 25 | err := repo.Get(&word, query, ID) 26 | if err != nil { 27 | return word, err 28 | } 29 | 30 | return word, nil 31 | } 32 | 33 | func (repo *Repository) Words(UserID string) ([]entities.Word, error) { 34 | query := ` 35 | SELECT id, title, description, from_language, to_language, created_at, updated_at, user_id 36 | FROM words 37 | WHERE user_id = $1 38 | ORDER BY created_at DESC 39 | ` 40 | 41 | var words []entities.Word 42 | err := repo.Select(&words, query, UserID) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | return words, nil 48 | } 49 | 50 | func (repo *Repository) UpdateWord(ID, Title, Description, UserID string, UpdatedAt time.Time) error { 51 | query := ` 52 | UPDATE words 53 | SET title = :title, 54 | description = :description, 55 | updated_at = :updated_at 56 | WHERE id = :id AND user_id = :user_id 57 | ` 58 | 59 | params := map[string]interface{}{ 60 | "id": ID, 61 | "title": Title, 62 | "description": Description, 63 | "user_id": UserID, 64 | "updated_at": UpdatedAt, 65 | } 66 | 67 | _, err := repo.NamedExec(query, params) 68 | return err 69 | } 70 | 71 | func (repo *Repository) DeleteWord(ID, UserID string) error { 72 | query := ` 73 | DELETE FROM words 74 | WHERE id = $1 AND user_id = $2 75 | ` 76 | 77 | result, err := repo.Exec(query, ID, UserID) 78 | if err != nil { 79 | return err 80 | } 81 | 82 | rowsAffected, err := result.RowsAffected() 83 | if err != nil { 84 | return err 85 | } 86 | 87 | if rowsAffected == 0 { 88 | return sql.ErrNoRows 89 | } 90 | 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "time" 7 | "word/config" 8 | ) 9 | 10 | func (h *Handler) GoogleLoginURL(w http.ResponseWriter, r *http.Request) { 11 | url := h.s.User.GoogleLoginURL() 12 | http.Redirect(w, r, url, http.StatusTemporaryRedirect) 13 | } 14 | 15 | func (h *Handler) GoogleCallback(w http.ResponseWriter, r *http.Request) { 16 | state := r.FormValue("state") 17 | code := r.FormValue("code") 18 | UserID, Email, err := h.s.User.GoogleCallback(state, code) 19 | if err != nil { 20 | http.Error(w, err.Error(), http.StatusBadRequest) 21 | return 22 | } 23 | token, err := CreateJWT(UserID, Email) 24 | if err != nil { 25 | http.Error(w, "Token not created", http.StatusBadRequest) 26 | return 27 | } 28 | cookie := createCookie(time.Now().Add(24*60*time.Hour), "Authorization", token) 29 | http.SetCookie(w, &cookie) 30 | http.Redirect(w, r, config.RedirectUser, http.StatusTemporaryRedirect) 31 | 32 | } 33 | 34 | func (h *Handler) Me(w http.ResponseWriter, r *http.Request) { 35 | UserID, Email, err := CheckAuth(r) 36 | if err != nil { 37 | http.Error(w, err.Error(), http.StatusBadRequest) 38 | return 39 | } 40 | user, languages, err := h.s.User.User(UserID, Email) 41 | if err != nil { 42 | http.Error(w, err.Error(), http.StatusBadRequest) 43 | return 44 | } 45 | data := map[string]interface{}{ 46 | "user": user, 47 | "languages": languages, 48 | } 49 | w.WriteHeader(http.StatusOK) 50 | w.Header().Set("Content-Type", "application/json") 51 | json.NewEncoder(w).Encode(data) 52 | } 53 | 54 | type OnboardBody struct { 55 | OsLanguage string `json:"os_language"` 56 | TargetLanguages []string `json:"target_languages"` 57 | } 58 | 59 | func (h *Handler) OnboardUpdate(w http.ResponseWriter, r *http.Request) { 60 | UserID, _, err := CheckAuth(r) 61 | if err != nil { 62 | http.Error(w, err.Error(), http.StatusBadRequest) 63 | return 64 | } 65 | var body OnboardBody 66 | decoder := json.NewDecoder(r.Body) 67 | defer r.Body.Close() 68 | err = decoder.Decode(&body) 69 | if err != nil { 70 | http.Error(w, "Incorrect body", http.StatusBadRequest) 71 | return 72 | } 73 | 74 | err = h.s.User.UpdateLanguages(body.OsLanguage, body.TargetLanguages, UserID) 75 | if err != nil { 76 | http.Error(w, err.Error(), http.StatusBadRequest) 77 | return 78 | } 79 | w.WriteHeader(http.StatusAccepted) 80 | } 81 | -------------------------------------------------------------------------------- /internal/service/play/play_test.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "word/internal/entities" 7 | ) 8 | 9 | func TestPlay(t *testing.T) { 10 | expect_strings := getUniqueArray("gotest", 20) 11 | play := New(&MockDB{}) 12 | 13 | //Get all 20 words 14 | got_words, err := play.GeneratePlay("", 20, "english") 15 | if err != nil { 16 | t.Errorf("Error: expected: nil, got: %v", err.Error()) 17 | } 18 | if len(got_words) != 20 { 19 | t.Errorf("Error: expected: 20, got: %v", len(got_words)) 20 | } 21 | for _, got_word := range got_words { 22 | if ok := containsString(expect_strings, got_word.Title); !ok { 23 | t.Errorf("Error: expected: true, got: %v", got_word.Title) 24 | } 25 | } 26 | //Get 10 words 27 | got_words, err = play.GeneratePlay("", 10, "english") 28 | if err != nil { 29 | t.Errorf("Error: expected: nil, got: %v", err.Error()) 30 | } 31 | if len(got_words) != 10 { 32 | t.Errorf("Error: expected: 20, got: %v", len(got_words)) 33 | } 34 | 35 | //Get 0 words 36 | got_words, err = play.GeneratePlay("", 0, "english") 37 | if err != nil { 38 | t.Errorf("Error: expected: nil, got: %v", err.Error()) 39 | } 40 | if len(got_words) != 0 { 41 | t.Errorf("Error: expected: 20, got: %v", len(got_words)) 42 | } 43 | 44 | //Get 100 words 45 | got_words, err = play.GeneratePlay("", 100, "english") 46 | if err != nil { 47 | t.Errorf("Error: expected: nil, got: %v", err.Error()) 48 | } 49 | if len(got_words) != 20 { 50 | t.Errorf("Error: expected: 20, got: %v", len(got_words)) 51 | } 52 | 53 | //Get words with not existing language 54 | got_words, err = play.GeneratePlay("", 20, "german") 55 | if err == nil { 56 | t.Errorf("Error: expected: error, got: nil") 57 | } 58 | } 59 | 60 | type MockDB struct { 61 | } 62 | 63 | func (db *MockDB) Words(UserID string) ([]entities.Word, error) { 64 | word_titles := getUniqueArray("gotest", 20) 65 | words := make([]entities.Word, 0, 20) 66 | for _, word_title := range word_titles { 67 | words = append(words, entities.Word{ 68 | WordBasic: entities.WordBasic{ 69 | ToLanguage: "english", 70 | Title: word_title, 71 | }, 72 | }) 73 | } 74 | return words, nil 75 | } 76 | 77 | func getUniqueArray(Word string, Len int) []string { 78 | array := make([]string, 0, Len) 79 | for i := 0; i < Len; i++ { 80 | array = append(array, Word+fmt.Sprint(i)) 81 | } 82 | return array 83 | } 84 | 85 | func containsString(slice []string, item string) bool { 86 | for _, v := range slice { 87 | if v == item { 88 | return true 89 | } 90 | } 91 | return false 92 | } 93 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= 2 | cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= 3 | filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= 4 | github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= 5 | github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= 8 | github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= 9 | github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 10 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 11 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 12 | github.com/jmoiron/sqlx v1.4.0 h1:1PLqN7S1UYp5t4SrVVnt4nUVNemrDAtxlulVe+Qgm3o= 13 | github.com/jmoiron/sqlx v1.4.0/go.mod h1:ZrZ7UsYB/weZdl2Bxg6jCRO9c3YHl8r3ahlKmRT4JLY= 14 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 15 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 16 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= 17 | github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= 18 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= 19 | github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= 20 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 21 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 22 | github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 23 | github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 24 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 25 | github.com/sashabaranov/go-openai v1.26.0 h1:upM565hxdqvCxNzuAcEBZ1XsfGehH0/9kgk9rFVpDxQ= 26 | github.com/sashabaranov/go-openai v1.26.0/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= 27 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 28 | golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= 29 | golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= 30 | -------------------------------------------------------------------------------- /internal/service/play/main.go: -------------------------------------------------------------------------------- 1 | package play 2 | 3 | import ( 4 | "errors" 5 | "math/rand" 6 | "word/internal/entities" 7 | ) 8 | 9 | type PlayService struct { 10 | db DB 11 | } 12 | 13 | type DB interface { 14 | Words(string) ([]entities.Word, error) 15 | } 16 | 17 | func New(db DB) *PlayService { 18 | return &PlayService{db} 19 | } 20 | 21 | // GeneratePlay generates a array of random word to play, repeat 22 | func (s *PlayService) GeneratePlay(UserID string, count int, language string) ([]entities.Word, error) { 23 | words, err := s.db.Words(UserID) 24 | if err != nil { 25 | return nil, errors.New("Error get users") 26 | } 27 | 28 | //Get only the words, those match with users language 29 | words_match_language := make([]entities.Word, 0) 30 | for _, word := range words { 31 | if word.ToLanguage == language { 32 | words_match_language = append(words_match_language, word) 33 | } 34 | } 35 | 36 | if len(words_match_language) == 0 { 37 | return nil, errors.New("Error there are not matched words") 38 | } else if len(words_match_language) < count { 39 | return words_match_language, nil 40 | } 41 | 42 | return getPlayWords(words_match_language, count), nil 43 | } 44 | 45 | // getPlayWords returns a new list of random words with ratio 7/3/2 (7 new words, 3 old words, 2 very old words) 46 | func getPlayWords(from []entities.Word, count int) []entities.Word { 47 | to := make([]entities.Word, 0, count) 48 | newWords, mediumWords, oldWords := splitSlice(from) 49 | 50 | ratio1, ratio2, ratio3 := 7, 3, 2 51 | totalRatio := ratio1 + ratio2 + ratio3 52 | countPart1 := count * ratio1 / totalRatio 53 | countPart2 := count * ratio2 / totalRatio 54 | countPart3 := count - countPart1 - countPart2 55 | selectRandomElements(newWords, &to, countPart1) 56 | selectRandomElements(mediumWords, &to, countPart2) 57 | selectRandomElements(oldWords, &to, countPart3) 58 | 59 | return to 60 | } 61 | 62 | // selectRandomElements writes n random element from first array to second array 63 | func selectRandomElements(from []entities.Word, to *[]entities.Word, count int) { 64 | if count > len(from) { 65 | count = len(from) 66 | } 67 | 68 | indices := rand.Perm(len(from))[:count] 69 | 70 | for _, i := range indices { 71 | *to = append(*to, from[i]) 72 | } 73 | 74 | } 75 | 76 | // splitSlice splits main array to 3 new small arrays with ratio 7/3/2 77 | func splitSlice[T any](original []T) ([]T, []T, []T) { 78 | totalLength := len(original) 79 | ratio1, ratio2, ratio3 := 7, 3, 2 80 | totalRatio := ratio1 + ratio2 + ratio3 81 | 82 | size1 := totalLength * ratio1 / totalRatio 83 | size2 := totalLength * ratio2 / totalRatio 84 | 85 | slice1 := original[:size1] 86 | slice2 := original[size1 : size1+size2] 87 | slice3 := original[size1+size2:] 88 | 89 | return slice1, slice2, slice3 90 | } 91 | -------------------------------------------------------------------------------- /internal/transport/rest/handler/word.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "word/internal/entities" 7 | ) 8 | 9 | func (h *Handler) Words(w http.ResponseWriter, r *http.Request) { 10 | UserID, _, err := CheckAuth(r) 11 | if err != nil { 12 | http.Error(w, err.Error(), http.StatusBadRequest) 13 | return 14 | } 15 | user_words, err := h.s.Word.UserWords(UserID) 16 | if err != nil { 17 | http.Error(w, err.Error(), http.StatusBadRequest) 18 | return 19 | } 20 | 21 | w.WriteHeader(http.StatusOK) 22 | json.NewEncoder(w).Encode(user_words) 23 | } 24 | 25 | func (h *Handler) CreateWord(w http.ResponseWriter, r *http.Request) { 26 | UserID, _, err := CheckAuth(r) 27 | if err != nil { 28 | http.Error(w, err.Error(), http.StatusBadRequest) 29 | return 30 | } 31 | var word entities.WordBasic 32 | decoder := json.NewDecoder(r.Body) 33 | defer r.Body.Close() 34 | 35 | err = decoder.Decode(&word) 36 | if err != nil { 37 | http.Error(w, "Incorrect body", http.StatusBadRequest) 38 | return 39 | } 40 | 41 | err = h.s.Word.CreateManualWord(word, UserID) 42 | if err != nil { 43 | http.Error(w, err.Error(), http.StatusBadRequest) 44 | return 45 | } 46 | w.WriteHeader(http.StatusCreated) 47 | json.NewEncoder(w).Encode(word) 48 | } 49 | 50 | func (h *Handler) Word(w http.ResponseWriter, r *http.Request) { 51 | ID := r.PathValue("id") 52 | 53 | word, err := h.s.Word.Word(ID) 54 | if err != nil { 55 | http.Error(w, err.Error(), http.StatusBadRequest) 56 | return 57 | } 58 | 59 | w.WriteHeader(http.StatusOK) 60 | json.NewEncoder(w).Encode(word) 61 | } 62 | 63 | func (h *Handler) DeleteWord(w http.ResponseWriter, r *http.Request) { 64 | UserID, _, err := CheckAuth(r) 65 | if err != nil { 66 | http.Error(w, err.Error(), http.StatusBadRequest) 67 | return 68 | } 69 | ID := r.PathValue("id") 70 | 71 | err = h.s.Word.DeleteWord(ID, UserID) 72 | if err != nil { 73 | http.Error(w, err.Error(), http.StatusBadRequest) 74 | return 75 | } 76 | w.WriteHeader(http.StatusAccepted) 77 | } 78 | 79 | func (h *Handler) UpdateWord(w http.ResponseWriter, r *http.Request) { 80 | UserID, _, err := CheckAuth(r) 81 | if err != nil { 82 | http.Error(w, err.Error(), http.StatusBadRequest) 83 | return 84 | } 85 | var word entities.WordBasic 86 | decoder := json.NewDecoder(r.Body) 87 | defer r.Body.Close() 88 | 89 | err = decoder.Decode(&word) 90 | if err != nil { 91 | http.Error(w, "Incorrect body", http.StatusBadRequest) 92 | return 93 | } 94 | 95 | err = h.s.Word.UpdateWord(word.ID, word.Title, word.Description, UserID) 96 | if err != nil { 97 | http.Error(w, err.Error(), http.StatusBadRequest) 98 | return 99 | } 100 | 101 | w.WriteHeader(http.StatusAccepted) 102 | } 103 | -------------------------------------------------------------------------------- /internal/storage/sqlite/word.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "time" 6 | "word/internal/entities" 7 | 8 | sq "github.com/Masterminds/squirrel" 9 | ) 10 | 11 | func (repo *Repository) CreateWord(word entities.Word) error { 12 | query, args, err := sq.Insert("words"). 13 | Columns("id", "title", "description", "from_language", "to_language", "type", "created_at", "updated_at", "user_id"). 14 | Values(word.ID, word.Title, word.Description, word.FromLanguage, word.ToLanguage, word.Type, word.CreatedAt, word.UpdatedAt, word.UserID). 15 | ToSql() 16 | if err != nil { 17 | return err 18 | } 19 | 20 | _, err = repo.db.Exec(query, args...) 21 | return err 22 | } 23 | 24 | func (repo *Repository) Word(ID string) (entities.Word, error) { 25 | query, args, err := sq.Select("id", "title", "description", "from_language", "to_language", "type", "created_at", "updated_at", "user_id"). 26 | From("words"). 27 | Where(sq.Eq{"id": ID}). 28 | ToSql() 29 | if err != nil { 30 | return entities.Word{}, err 31 | } 32 | 33 | var word entities.Word 34 | err = repo.db.Get(&word, query, args...) 35 | if err != nil { 36 | return word, err 37 | } 38 | 39 | return word, nil 40 | } 41 | 42 | func (repo *Repository) Words(UserID string) ([]entities.Word, error) { 43 | query, args, err := sq.Select("id", "title", "description", "from_language", "to_language", "created_at", "updated_at", "user_id"). 44 | From("words"). 45 | Where(sq.Eq{"user_id": UserID}). 46 | OrderBy("created_at DESC"). 47 | ToSql() 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | var words []entities.Word 53 | err = repo.db.Select(&words, query, args...) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | return words, nil 59 | } 60 | 61 | func (repo *Repository) UpdateWord(ID, Title, Description, UserID string, UpdatedAt time.Time) error { 62 | query, args, err := sq.Update("words"). 63 | SetMap(map[string]interface{}{ 64 | "title": Title, 65 | "description": Description, 66 | "updated_at": UpdatedAt, 67 | }). 68 | Where(sq.Eq{"id": ID, "user_id": UserID}). 69 | ToSql() 70 | if err != nil { 71 | return err 72 | } 73 | 74 | _, err = repo.db.Exec(query, args...) 75 | return err 76 | } 77 | 78 | func (repo *Repository) DeleteWord(ID, UserID string) error { 79 | query, args, err := sq.Delete("words"). 80 | Where(sq.Eq{"id": ID, "user_id": UserID}). 81 | ToSql() 82 | if err != nil { 83 | return err 84 | } 85 | 86 | result, err := repo.db.Exec(query, args...) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | rowsAffected, err := result.RowsAffected() 92 | if err != nil { 93 | return err 94 | } 95 | 96 | if rowsAffected == 0 { 97 | return sql.ErrNoRows 98 | } 99 | 100 | return nil 101 | } 102 | -------------------------------------------------------------------------------- /internal/service/word/main.go: -------------------------------------------------------------------------------- 1 | package word 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | "word/internal/entities" 7 | ) 8 | 9 | type WordService struct { 10 | db DB 11 | } 12 | 13 | type DB interface { 14 | Words(string) ([]entities.Word, error) 15 | Languages(string) ([]entities.Language, error) 16 | CreateWord(entities.Word) error 17 | Word(string) (entities.Word, error) 18 | DeleteWord(string, string) error 19 | UpdateWord(string, string, string, string, time.Time) error 20 | } 21 | 22 | func New(db DB) *WordService { 23 | return &WordService{db} 24 | } 25 | 26 | // UserWords returns all words those the user has, and splits them by language 27 | func (s *WordService) UserWords(UserID string) ([]entities.WordsWithLanguage, error) { 28 | languages, err := s.db.Languages(UserID) 29 | if err != nil { 30 | return nil, errors.New("Error get user languages") 31 | } 32 | words, err := s.db.Words(UserID) 33 | if err != nil { 34 | return nil, errors.New("Error get user words") 35 | } 36 | 37 | languagedWords := make([]entities.WordsWithLanguage, 0) 38 | for _, language := range languages { 39 | words_match_language := make([]entities.Word, 0) 40 | for _, word := range words { 41 | if word.ToLanguage == language.LanguageName { 42 | words_match_language = append(words_match_language, word) 43 | } 44 | } 45 | languagedWords = append(languagedWords, entities.WordsWithLanguage{ 46 | Words: words_match_language, 47 | Language: language.LanguageName, 48 | }) 49 | } 50 | 51 | return languagedWords, nil 52 | } 53 | 54 | // CreateManualWord creates a new word in storage 55 | func (s *WordService) CreateManualWord(WordFromUser entities.WordBasic, UserID string) error { 56 | word := entities.Word{ 57 | CreatedAt: time.Now(), 58 | UpdatedAt: time.Now(), 59 | UserID: UserID, 60 | WordBasic: entities.WordBasic{ 61 | ID: WordFromUser.ID, 62 | Type: WordFromUser.Type, 63 | FromLanguage: WordFromUser.FromLanguage, 64 | ToLanguage: WordFromUser.ToLanguage, 65 | Title: WordFromUser.Title, 66 | Description: WordFromUser.Description, 67 | }, 68 | } 69 | err := s.db.CreateWord(word) 70 | if err != nil { 71 | return errors.New("Error create word") 72 | } 73 | return nil 74 | } 75 | 76 | // Word return existing word by ID 77 | func (s *WordService) Word(ID string) (entities.Word, error) { 78 | word, err := s.db.Word(ID) 79 | if err != nil { 80 | return word, errors.New("Error get word") 81 | } 82 | return word, nil 83 | } 84 | 85 | // DeleteWord removes word by ID ans UserID 86 | func (s *WordService) DeleteWord(ID string, UserID string) error { 87 | err := s.db.DeleteWord(ID, UserID) 88 | if err != nil { 89 | return errors.New("Error delete word") 90 | } 91 | return nil 92 | } 93 | 94 | // UpdateWord updates existing word by ID and UserID 95 | func (s *WordService) UpdateWord(ID, Title, Description, UserID string) error { 96 | now := time.Now() 97 | err := s.db.UpdateWord(ID, Title, Description, UserID, now) 98 | if err != nil { 99 | return errors.New("Error update word") 100 | } 101 | return nil 102 | } 103 | -------------------------------------------------------------------------------- /internal/service/ask/ask_test.go: -------------------------------------------------------------------------------- 1 | package ask 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | "word/internal/entities" 7 | ) 8 | 9 | type TestWriter struct { 10 | count int 11 | } 12 | 13 | // NewTestWriter returns a new writer 14 | func NewTestWriter() *TestWriter { 15 | return &TestWriter{ 16 | count: 0, 17 | } 18 | } 19 | 20 | // Write writes count of write 21 | func (w *TestWriter) Write(p []byte) (int, error) { 22 | w.count = w.count + 1 23 | return 0, nil 24 | } 25 | 26 | var answer = `"Quite" в английском языке означает "довольно", "вполне" или "совсем". 27 | Это слово используется для усиления прилагательных и наречий, указывая на степень чего-либо. 28 | 29 | 1. She is quite talented. 30 | Она довольно талантлива. 31 | Здесь "quite" подчеркивает высокий уровень таланта. 32 | 33 | 2. The movie was quite interesting. 34 | Фильм был вполне интересным. 35 | В данном случае "quite" указывает на то, что фильм был действительно интересным, но не чрезмерно. 36 | 37 | 3. It's quite cold outside. 38 | На улице довольно холодно. 39 | Здесь "quite" подчеркивает, что температура заметно низкая.` 40 | 41 | func TestAsk(t *testing.T) { 42 | 43 | //Setting mocks 44 | db := &MockDB{} 45 | service := New(db) 46 | service.setTestTrue() 47 | 48 | ID := "some_id" 49 | UserID := "some_user_id" 50 | UserLanguage := "russian" 51 | TargetLanguage := "english" 52 | Word := "quit" 53 | 54 | w := NewTestWriter() 55 | 56 | err := service.GenerateWord(ID, UserID, UserLanguage, TargetLanguage, Word, w) 57 | 58 | if err != nil { 59 | t.Errorf("Ask: expected: nil, got:%v", err.Error()) 60 | } 61 | 62 | //counter count (count has to be incremented at least 10 times) 63 | if w.count <= 10 { 64 | t.Errorf("Ask: expected: >10, got:%v", w.count) 65 | } 66 | 67 | //Check the datas 68 | if db.word.ID != ID { 69 | t.Errorf("Ask: expected: %v, got:%v", ID, db.word.ID) 70 | } 71 | if db.word.UserID != UserID { 72 | t.Errorf("Ask: expected: %v, got:%v", UserID, db.word.UserID) 73 | } 74 | if db.word.FromLanguage != UserLanguage { 75 | t.Errorf("Ask: expected: %v, got:%v", UserLanguage, db.word.FromLanguage) 76 | } 77 | if db.word.ToLanguage != TargetLanguage { 78 | t.Errorf("Ask: expected: %v, got:%v", TargetLanguage, db.word.ToLanguage) 79 | } 80 | if db.word.Title != Word { 81 | t.Errorf("Ask: expected: %v, got:%v", Word, db.word.Title) 82 | } 83 | if db.word.Type != "ai" { 84 | t.Errorf("Ask: expected: %v, got:%v", "ai", db.word.Type) 85 | } 86 | if strings.TrimSpace(db.word.Description) != strings.TrimSpace(strings.Join(strings.Fields(answer), " ")+" ") { 87 | t.Errorf("Ask: expected: %v, got: %v", strings.TrimSpace(strings.Join(strings.Fields(answer), " ")+" "), strings.TrimSpace(db.word.Description)) 88 | } 89 | 90 | } 91 | 92 | type MockDB struct { 93 | word entities.Word 94 | } 95 | 96 | func (db *MockDB) CreateWord(Word entities.Word) error { 97 | db.word = Word 98 | return nil 99 | } 100 | 101 | var expected_user_prompt = `Объясните мне, что означает "something" на английском языке (если это другой язык, то переведите на английский). Сначала объясните в общих чертах, что означает это слово/фраза. Затем составьте 3 предложения на английском языке и перевод на русский. И объясните точно в контексте каждого предложения. Ответ нужен без разметки Markdown.` 102 | 103 | func TestPromptGenerator(t *testing.T) { 104 | Word := "something" 105 | UserLanguage := "russian" 106 | TargetLanguage := "english" 107 | 108 | user_prompt := promptGenarate(UserLanguage, TargetLanguage, Word) 109 | if user_prompt != expected_user_prompt { 110 | t.Errorf("PromptGenarate: expected: %v, got: %v", expected_user_prompt, user_prompt) 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /internal/service/ask/prompt-texts.go: -------------------------------------------------------------------------------- 1 | package ask 2 | 3 | type Targets struct { 4 | English string 5 | German string 6 | } 7 | 8 | var ( 9 | englishText Targets = Targets{ 10 | English: `Explain to me what "[[]]" means in English (if it is another language then translate to English). First explain in general what this word/phrase means. Then make 3 sentences in English, and a translation in English. And explain exactly in the context of each sentence. The answer is needed without Markdown markup.`, 11 | German: `Explain to me what "[[]]" means in German (if it is another language then translate to German). First explain in general what this word/phrase means. Then make 3 sentences in German, and a translation in English. And explain exactly in the context of each sentence. The answer is needed without Markdown markup.`, 12 | } 13 | russianText Targets = Targets{ 14 | English: `Объясните мне, что означает "[[]]" на английском языке (если это другой язык, то переведите на английский). Сначала объясните в общих чертах, что означает это слово/фраза. Затем составьте 3 предложения на английском языке и перевод на русский. И объясните точно в контексте каждого предложения. Ответ нужен без разметки Markdown.`, 15 | German: `Объясните мне, что означает "[[]]" на немецком языке (если это другой язык, то переведите на немецкий). Сначала объясните в общих чертах, что означает это слово/фраза. Затем составьте 3 предложения на немецком языке и перевод на русский. И объясните точно в контексте каждого предложения. Ответ нужен без разметки Markdown.`, 16 | } 17 | frenchText Targets = Targets{ 18 | English: `Expliquez-moi ce que signifie "[[]]" en anglais (s'il s'agit d'une autre langue, traduisez-la en anglais). Expliquez d'abord en général ce que ce mot/phrase signifie. Ensuite, faites 3 phrases en anglais, et une traduction en français. Et expliquez exactement dans le contexte de chaque phrase. La réponse est nécessaire sans balisage Markdown.`, 19 | German: `Expliquez-moi ce que signifie "[[]]" en allemand (s'il s'agit d'une autre langue, traduisez-la en allemand). Expliquez d'abord en général ce que ce mot/phrase signifie. Ensuite, faites 3 phrases en allemand, et une traduction en français. Et expliquez exactement dans le contexte de chaque phrase. La réponse est nécessaire sans balisage Markdown.`, 20 | } 21 | turkishText Targets = Targets{ 22 | English: `Bana "[[]]" ifadesinin İngilizcede ne anlama geldiğini açıklayın (eğer başka bir dil ise İngilizceye çevirin). Önce açıklayın Genel olarak bu kelimenin/cümlenin ne anlama geldiğini. Daha sonra İngilizce 3 cümle ve Türkçe bir çeviri yapın. Ve tam olarak açıklayın her cümle bağlamında. Cevap Markdown işaretlemesi olmadan gereklidir.`, 23 | German: `Bana "[[]]" ifadesinin Almanca'da ne anlama geldiğini açıklayın (eğer başka bir dilde ise Almanca'ya çevirin). Önce açıklayın Genel olarak bu kelimenin/cümlenin ne anlama geldiğini. Daha sonra Almanca 3 cümle ve Türkçe bir çeviri yapın. Ve tam olarak açıklayın her cümle bağlamında. Cevap Markdown işaretlemesi olmadan gereklidir.`, 24 | } 25 | chineseText Targets = Targets{ 26 | English: `请解释一下"[[]]"在英语中是什么意思(如果是其他语言,请翻译成英语)。首先解释这个词/短语的大致意思。然后用英语造 3 个句子,并翻译成中文。并准确解释每个句子的上下文。答案不需要 Markdown 标记。`, 27 | German: `请解释一下"[[]]"在德语中是什么意思(如果是其他语言,请翻译成德语)。首先解释这个词/短语的大致意思。然后用德语造 3 个句子,并翻译成中文。并准确解释每个句子的上下文。答案不需要 Markdown 标记。`, 28 | } 29 | ) 30 | 31 | func getPropmtWithLanguage(Lang string, TargetLang string) string { 32 | userLang := englishText 33 | switch Lang { 34 | case "english": 35 | userLang = englishText 36 | case "russian": 37 | userLang = russianText 38 | case "french": 39 | userLang = frenchText 40 | case "turkish": 41 | userLang = turkishText 42 | case "chinese": 43 | userLang = chineseText 44 | } 45 | 46 | result := userLang.English 47 | switch TargetLang { 48 | case "english": 49 | result = userLang.English 50 | case "german": 51 | result = userLang.German 52 | } 53 | 54 | return result 55 | } 56 | -------------------------------------------------------------------------------- /internal/service/ask/main.go: -------------------------------------------------------------------------------- 1 | package ask 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "io" 7 | "strings" 8 | "time" 9 | "word/config" 10 | "word/internal/entities" 11 | 12 | "github.com/sashabaranov/go-openai" 13 | ) 14 | 15 | // Service to create word with OpenAI API 16 | type AskService struct { 17 | db DB 18 | openai_client *openai.Client 19 | isTest bool 20 | } 21 | 22 | type DB interface { 23 | CreateWord(entities.Word) error 24 | } 25 | 26 | func New(db DB) *AskService { 27 | client := openai.NewClient(config.OpenaiToken) 28 | return &AskService{db: db, openai_client: client} 29 | } 30 | 31 | func (s *AskService) setTestTrue() { 32 | s.isTest = true 33 | } 34 | 35 | type Writer struct { 36 | result string 37 | stream io.Writer 38 | } 39 | 40 | // NewWriter returns new writer with field to final result and another writer to stream text 41 | func NewWriter(stream io.Writer) *Writer { 42 | return &Writer{ 43 | result: "", 44 | stream: stream, 45 | } 46 | } 47 | 48 | // Write writes current part of text to final result and writes to another writer 49 | func (w *Writer) Write(p []byte) (int, error) { 50 | w.result = w.result + string(p) 51 | n, err := w.stream.Write(p) 52 | return n, err 53 | } 54 | 55 | func (s *AskService) GenerateWord(ID, UserID, UserLanguage, TargetLanguage, Word string, Writer io.Writer) error { 56 | w := NewWriter(Writer) 57 | 58 | //Generate prompt to openai api from user data 59 | user_prompt := promptGenarate(UserLanguage, TargetLanguage, Word) 60 | 61 | //To testing AskService without connection to openai API 62 | if !s.isTest { 63 | s.runOpenaiAPI(user_prompt, w) 64 | } else { 65 | s.runMockAPI(w) 66 | } 67 | 68 | word := entities.Word{ 69 | WordBasic: entities.WordBasic{ 70 | ID: ID, 71 | Title: Word, 72 | Description: w.result, 73 | FromLanguage: UserLanguage, 74 | ToLanguage: TargetLanguage, 75 | Type: "ai", 76 | }, 77 | CreatedAt: time.Now(), 78 | UpdatedAt: time.Now(), 79 | UserID: UserID, 80 | } 81 | 82 | err := s.db.CreateWord(word) 83 | if err != nil { 84 | return errors.New("error create word") 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (s *AskService) runOpenaiAPI(UserPrompt string, w io.Writer) { 91 | ctx := context.Background() 92 | req := openai.ChatCompletionRequest{ 93 | Model: openai.GPT3Dot5Turbo, 94 | Messages: []openai.ChatCompletionMessage{ 95 | { 96 | Role: openai.ChatMessageRoleUser, 97 | Content: UserPrompt, 98 | }, 99 | }, 100 | Stream: true, 101 | Temperature: 1, 102 | TopP: 1, 103 | } 104 | stream, err := s.openai_client.CreateChatCompletionStream(ctx, req) 105 | if err != nil { 106 | return 107 | } 108 | defer stream.Close() 109 | 110 | for { 111 | response, err := stream.Recv() 112 | if errors.Is(err, io.EOF) { 113 | break 114 | } 115 | 116 | if err != nil { 117 | return 118 | } 119 | 120 | _, err = w.Write([]byte(response.Choices[0].Delta.Content)) 121 | if err != nil { 122 | return 123 | } 124 | } 125 | } 126 | 127 | // mock function to testing 128 | func (s *AskService) runMockAPI(w io.Writer) { 129 | words := strings.Fields(`"Quite" в английском языке означает "довольно", "вполне" или "совсем". 130 | Это слово используется для усиления прилагательных и наречий, указывая на степень чего-либо. 131 | 132 | 1. She is quite talented. 133 | Она довольно талантлива. 134 | Здесь "quite" подчеркивает высокий уровень таланта. 135 | 136 | 2. The movie was quite interesting. 137 | Фильм был вполне интересным. 138 | В данном случае "quite" указывает на то, что фильм был действительно интересным, но не чрезмерно. 139 | 140 | 3. It's quite cold outside. 141 | На улице довольно холодно. 142 | Здесь "quite" подчеркивает, что температура заметно низкая.`) 143 | for _, word := range words { 144 | _, err := w.Write([]byte(word + " ")) 145 | if err != nil { 146 | break 147 | } 148 | time.Sleep(50 * time.Millisecond) 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /internal/service/user/main.go: -------------------------------------------------------------------------------- 1 | package user 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "time" 9 | "word/config" 10 | "word/internal/entities" 11 | 12 | "github.com/google/uuid" 13 | "golang.org/x/oauth2" 14 | ) 15 | 16 | // Service for google oauth2, get user and update user languages 17 | type UserService struct { 18 | db DB 19 | } 20 | 21 | type DB interface { 22 | CreateUser(entities.User) error 23 | UserByEmail(string) (entities.User, error) 24 | Languages(string) ([]entities.Language, error) 25 | UpdateUserLanguage(string, string) error 26 | CreateLanguages([]entities.Language) error 27 | } 28 | 29 | func New(db DB) *UserService { 30 | return &UserService{db} 31 | } 32 | 33 | // GoogleLoginURL returns url to redirect 34 | func (s *UserService) GoogleLoginURL() string { 35 | return config.OAuthConfig.AuthCodeURL(config.OAuthState) 36 | } 37 | 38 | // GoogleCallback return info about user (ID, Email) from state and code 39 | func (s *UserService) GoogleCallback(state string, code string) (string, string, error) { 40 | 41 | if state != config.OAuthState { 42 | return "", "", errors.New("State does not match") 43 | } 44 | token, err := config.OAuthConfig.Exchange(context.Background(), code) 45 | if err != nil { 46 | return "", "", errors.New("Exchang failed") 47 | } 48 | userInfo, err := s.getGoogleUserInfo(token) 49 | if err != nil { 50 | return "", "", err 51 | } 52 | 53 | //Parcing fields from interface{} type 54 | userEmail, ok := userInfo["email"].(string) 55 | if !ok { 56 | return "", "", errors.New("Fields parcing failed") 57 | } 58 | userAvatar, ok := userInfo["picture"].(string) 59 | if !ok { 60 | return "", "", errors.New("Fields parcing failed") 61 | } 62 | userFullName, ok := userInfo["name"].(string) 63 | if !ok { 64 | return "", "", errors.New("Fields parcing failed") 65 | } 66 | userFirstName, ok := userInfo["given_name"].(string) 67 | if !ok { 68 | return "", "", errors.New("Fields parcing failed") 69 | } 70 | 71 | user := entities.User{ 72 | ID: uuid.New().String(), 73 | Name: userFirstName, 74 | FullName: userFullName, 75 | Email: userEmail, 76 | Avatar: userAvatar, 77 | Language: "", 78 | } 79 | 80 | err = s.db.CreateUser(user) 81 | if err == nil { 82 | return user.ID, user.Email, nil 83 | } 84 | 85 | user, err = s.db.UserByEmail(user.Email) 86 | if err != nil { 87 | return "", "", errors.New("Error on get user") 88 | } 89 | 90 | return user.ID, user.Email, nil 91 | } 92 | 93 | // getGoogleUserInfo returns map of info about user 94 | func (s *UserService) getGoogleUserInfo(token *oauth2.Token) (map[string]interface{}, error) { 95 | client := config.OAuthConfig.Client(context.Background(), token) 96 | 97 | resp, err := client.Get("https://www.googleapis.com/oauth2/v3/userinfo") 98 | if err != nil { 99 | return nil, fmt.Errorf("failed to get userinfo: %s", err) 100 | } 101 | defer resp.Body.Close() 102 | 103 | var userInfo map[string]interface{} 104 | err = json.NewDecoder(resp.Body).Decode(&userInfo) 105 | if err != nil { 106 | return nil, fmt.Errorf("failed to parse userinfo response: %s", err) 107 | } 108 | 109 | return userInfo, nil 110 | } 111 | 112 | // User returns data about user and his languages 113 | func (s *UserService) User(UserID string, Email string) (entities.User, []entities.Language, error) { 114 | user, err := s.db.UserByEmail(Email) 115 | if err != nil { 116 | return entities.User{}, nil, errors.New("Error get user") 117 | } 118 | languages, err := s.db.Languages(UserID) 119 | if err != nil { 120 | return entities.User{}, nil, errors.New("Error get languages") 121 | } 122 | return user, languages, nil 123 | } 124 | 125 | // UpdateLanguages updates users system language and the learning languages 126 | func (s *UserService) UpdateLanguages(UserLanguage string, TargetLanguages []string, UserID string) error { 127 | err := s.db.UpdateUserLanguage(UserLanguage, UserID) 128 | if err != nil { 129 | return errors.New("Error update user language") 130 | } 131 | 132 | //Creating new languages 133 | languages := make([]entities.Language, 0) 134 | for _, target_language := range TargetLanguages { 135 | new_language := entities.Language{ 136 | ID: uuid.New().String(), 137 | UserID: UserID, 138 | LanguageName: target_language, 139 | CreatedAt: time.Now(), 140 | } 141 | languages = append(languages, new_language) 142 | } 143 | 144 | err = s.db.CreateLanguages(languages) 145 | if err != nil { 146 | return errors.New("Error create target languages") 147 | } 148 | 149 | return nil 150 | } 151 | --------------------------------------------------------------------------------