Skip to content

Commit

Permalink
Feat/telegram bot (#16)
Browse files Browse the repository at this point in the history
* ✨ feat: Add settings page and UI components

- Added new settings page with profile settings section
- Created new UI components: Checkbox, Select, and Switch
- Updated package.json with new Radix UI dependencies
- Added settings.html entry point
- Updated Vite config to include settings.html

* 🔧 refactor: Remove chatbot functionality and refactor handlers (main)

- Removed chatbot-related code including the `NewChatBot` function and associated handlers
- Moved `initHandlerRequest` function to a separate `handler.go` file
- Added job insertion logic to `saveBookmark` function in `websummary.go`
- Updated error handling in bot middleware to conditionally print stack traces in debug mode
- Removed chatbot initialization from main.go

* ✨ feat: Add Telegram account linking functionality

- Added new SQL query to get OAuth connection by provider and provider ID
- Updated OAuth connection update query to include user ID
- Implemented owner transfer functionality for bookmarks
- Added new handler for linking Telegram account
- Updated bot commands to include link account option
- Added UI components for linking Telegram account in profile settings
- Updated dependencies to include js-cookie for token management
  • Loading branch information
vaayne committed Dec 30, 2024
1 parent 193c9ff commit d3abcfc
Show file tree
Hide file tree
Showing 29 changed files with 697 additions and 267 deletions.
9 changes: 7 additions & 2 deletions database/queries/auth.sql
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ RETURNING *;
SELECT * FROM auth_user_oauth_connections
WHERE user_id = $1 AND provider = $2;

-- name: GetOAuthConnectionByProviderAndProviderID :one
SELECT * FROM auth_user_oauth_connections
WHERE provider = $1 AND provider_user_id = $2;

-- name: GetUserByOAuthProviderId :one
SELECT * FROM users
WHERE uuid = (SELECT user_id FROM auth_user_oauth_connections WHERE provider = $1 AND provider_user_id = $2);
Expand All @@ -56,8 +60,9 @@ UPDATE auth_user_oauth_connections SET
access_token = $4,
refresh_token = $5,
token_expires_at = $6,
provider_data = $7
WHERE user_id = $1 AND provider = $2
provider_data = $7,
user_id = $8
WHERE provider_user_id = $1 AND provider = $2
RETURNING *;

-- name: DeleteOAuthConnection :exec
Expand Down
7 changes: 7 additions & 0 deletions database/queries/bookmarks.sql
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ DELETE FROM bookmarks WHERE uuid = $1 AND user_id = $2;

-- name: DeleteBookmarksByUser :exec
DELETE FROM bookmarks WHERE user_id = $1;

-- name: OwnerTransferBookmark :exec
UPDATE bookmarks
SET
user_id = $2,
updated_at = CURRENT_TIMESTAMP
WHERE user_id = $1;
2 changes: 1 addition & 1 deletion internal/core/queue/crawler_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func (w *CrawlerWorker) Work(ctx context.Context, job *river.Job[CrawlerWorkerAr
return err
}

if dto.Content != "" {
if dto.Content != "" && dto.Summary == "" {
dto, err = svc.SummarierContent(ctx, tx, job.Args.ID, job.Args.UserID)
if err != nil {
logger.FromContext(ctx).Error("failed to summarise content", "error", err)
Expand Down
3 changes: 3 additions & 0 deletions internal/pkg/auth/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type dto interface {
DeleteUserById(ctx context.Context, db db.DBTX, argUuid uuid.UUID) error
GetAPIKeyByPrefix(ctx context.Context, db db.DBTX, arg db.GetAPIKeyByPrefixParams) (db.AuthApiKey, error)
GetOAuthConnectionByUserAndProvider(ctx context.Context, db db.DBTX, arg db.GetOAuthConnectionByUserAndProviderParams) (db.AuthUserOauthConnection, error)
GetOAuthConnectionByProviderAndProviderID(ctx context.Context, db db.DBTX, arg db.GetOAuthConnectionByProviderAndProviderIDParams) (db.AuthUserOauthConnection, error)
GetUserByEmail(ctx context.Context, db db.DBTX, email pgtype.Text) (db.User, error)
GetUserById(ctx context.Context, db db.DBTX, argUuid uuid.UUID) (db.User, error)
GetUserByOAuthProviderId(ctx context.Context, db db.DBTX, arg db.GetUserByOAuthProviderIdParams) (db.User, error)
Expand All @@ -35,4 +36,6 @@ type dto interface {
UpdateAPIKeyLastUsed(ctx context.Context, db db.DBTX, id uuid.UUID) error
UpdateOAuthConnection(ctx context.Context, db db.DBTX, arg db.UpdateOAuthConnectionParams) (db.AuthUserOauthConnection, error)
UpdateUserById(ctx context.Context, db db.DBTX, arg db.UpdateUserByIdParams) (db.User, error)

OwnerTransferBookmark(ctx context.Context, db db.DBTX, arg db.OwnerTransferBookmarkParams) error
}
42 changes: 42 additions & 0 deletions internal/pkg/auth/oauth.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ func (s *Service) HandleOAuth2Callback(ctx context.Context, tx db.DBTX, provider
params := db.UpdateOAuthConnectionParams{
UserID: dbUser.Uuid,
Provider: provider,
ProviderUserID: oUser.ID,
AccessToken: pgtype.Text{String: token.AccessToken, Valid: token.AccessToken != ""},
RefreshToken: pgtype.Text{String: token.RefreshToken, Valid: token.RefreshToken != ""},
TokenExpiresAt: pgtype.Timestamptz{Time: token.Expiry, Valid: true},
Expand All @@ -125,3 +126,44 @@ func (s *Service) HandleOAuth2Callback(ctx context.Context, tx db.DBTX, provider
user.Load(&dbUser)
return user, nil
}

func (s *Service) LinkAccount(ctx context.Context, tx db.DBTX, oAuthUser OAuth2User, jwtString string) error {
userId, _, err := ValidateJWT(jwtString)
if err != nil {
return fmt.Errorf("invalid jwt: %w", err)
}

oAuthConn, err := s.dao.GetOAuthConnectionByProviderAndProviderID(ctx, tx, db.GetOAuthConnectionByProviderAndProviderIDParams{
Provider: oAuthUser.Provider,
ProviderUserID: oAuthUser.ID,
})
if err != nil && !db.IsNotFoundError(err) {
return fmt.Errorf("get oauth connection by provider and provider id failed: %w", err)
}
originalUserId := oAuthConn.UserID
// TODO: move all resources from the original user to the new user
if err := s.OwnerTransfer(ctx, tx, originalUserId, userId); err != nil {
return fmt.Errorf("owner transfer failed: %w", err)
}

// mark the original user as linked to the new user
updateUserParams := db.UpdateUserByIdParams{
Status: "linked to " + userId.String(),
}
if _, err = s.dao.UpdateUserById(ctx, tx, updateUserParams); err != nil {
return fmt.Errorf("update user by id failed: %w", err)
}

// update oauth connection to link to the new user
params := db.UpdateOAuthConnectionParams{
UserID: userId,
Provider: oAuthUser.Provider,
ProviderUserID: oAuthUser.ID,
}
_, err = s.dao.UpdateOAuthConnection(ctx, tx, params)
if err != nil {
return fmt.Errorf("update oauth connection failed: %w", err)
}

return nil
}
16 changes: 16 additions & 0 deletions internal/pkg/auth/ownertransfer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package auth

import (
"context"
"recally/internal/pkg/db"

"github.com/google/uuid"
"github.com/jackc/pgx/v5/pgtype"
)

func (s *Service) OwnerTransfer(ctx context.Context, tx db.DBTX, ownerID, newOwnerID uuid.UUID) error {
return s.dao.OwnerTransferBookmark(ctx, tx, db.OwnerTransferBookmarkParams{
UserID: pgtype.UUID{Bytes: ownerID, Valid: ownerID != uuid.Nil},
UserID_2: pgtype.UUID{Bytes: newOwnerID, Valid: newOwnerID != uuid.Nil},
})
}
3 changes: 2 additions & 1 deletion internal/pkg/auth/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ func (s *Service) GetTelegramUser(ctx context.Context, tx db.DBTX, userID string
func (s *Service) CreateTelegramUser(ctx context.Context, tx db.DBTX, userName, userID string) (*UserDTO, error) {
// create user
user, err := s.dao.CreateUser(ctx, tx, db.CreateUserParams{
Status: "active",
Username: pgtype.Text{String: userName, Valid: true},
Status: "active",
})
if err != nil {
return nil, fmt.Errorf("failed to create telegram user: %w", err)
Expand Down
40 changes: 36 additions & 4 deletions internal/pkg/db/auth.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 18 additions & 0 deletions internal/pkg/db/bookmarks.sql.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 0 additions & 56 deletions internal/port/bots/chatbot.go

This file was deleted.

41 changes: 41 additions & 0 deletions internal/port/bots/handlers/handler.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package handlers

import (
"context"
"fmt"
"recally/internal/pkg/auth"
"recally/internal/pkg/contexts"
"recally/internal/pkg/db"
"strings"

"github.com/jackc/pgx/v5"
"gopkg.in/telebot.v3"
)

func (h *Handler) initHandlerRequest(c telebot.Context) (context.Context, *auth.UserDTO, db.DBTX, error) {
ctx := c.Get(contexts.ContextKeyContext).(context.Context)

tx, ok := contexts.Get[pgx.Tx](ctx, contexts.ContextKeyTx)
if !ok {
return nil, nil, nil, fmt.Errorf("failed to get dbtx from context")
}

userID, ok := contexts.Get[string](ctx, contexts.ContextKeyUserID)
if !ok {
return nil, nil, nil, fmt.Errorf("failed to get userID from context")
}
user, err := h.authService.GetTelegramUser(ctx, tx, userID)
if err != nil {
if strings.Contains(err.Error(), db.ErrNotFound) {
userName := ctx.Value(contexts.ContextKey(contexts.ContextKeyUserName)).(string)
user, err = h.authService.CreateTelegramUser(ctx, tx, userName, userID)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to create user: %w", err)
}
} else {
return nil, nil, nil, fmt.Errorf("failed to get user: %w", err)
}
}

return ctx, user, tx, nil
}
46 changes: 46 additions & 0 deletions internal/port/bots/handlers/linkaccount.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package handlers

import (
"context"
"recally/internal/pkg/auth"
"recally/internal/pkg/contexts"
"strings"

"github.com/jackc/pgx/v5"
tele "gopkg.in/telebot.v3"
)

func (h *Handler) LinkAccountHandler(c tele.Context) error {
ctx := c.Get(contexts.ContextKeyContext).(context.Context)

tx, ok := contexts.Get[pgx.Tx](ctx, contexts.ContextKeyTx)
if !ok {
_ = c.Reply("Failed to get dbtx from context")
return nil
}

userID, ok := contexts.Get[string](ctx, contexts.ContextKeyUserID)
if !ok {
_ = c.Reply("Failed to get userID from context")
return nil
}

token := strings.TrimSpace(strings.TrimPrefix(c.Text(), "/linkaccount"))
if token == "" {
_ = c.Reply("Invalid token")
return nil
}

oAuthUser := auth.OAuth2User{
Provider: "telegram",
ID: userID,
Name: c.Sender().Username,
}

if err := h.authService.LinkAccount(ctx, tx, oAuthUser, token); err != nil {
_ = c.Reply("Failed to link user: " + err.Error())
return nil
}

return c.Reply("User linked successfully")
}
Loading

0 comments on commit d3abcfc

Please sign in to comment.