Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 81 additions & 0 deletions internal/bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (

"leetcode-telegram-bot/internal/config"
"leetcode-telegram-bot/internal/database"
"leetcode-telegram-bot/internal/leetcode"
"leetcode-telegram-bot/internal/models"

tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
Expand Down Expand Up @@ -88,6 +89,8 @@ func (b *Bot) handleMessage(message *tgbotapi.Message) {
b.handleStatusCommand(message)
case "resetday":
b.handleResetDayCommand(message)
case "register":
b.handleRegisterLeetcodeProfile(message)
default:
b.sendMessage(message.Chat.ID, "Unknown command. Use /help to see available commands.")
}
Expand Down Expand Up @@ -445,3 +448,81 @@ func (b *Bot) SendReminder() error {
log.Printf("Sent reminder to %d users for Day %d", len(users), dayNumber)
return nil
}

func (b *Bot) CheckSubmissions() error {
today := time.Now().Format("2006-01-02")

// Get users who haven't submitted today
users, err := b.db.GetUsersWhoDidntSubmitToday(today)
if err != nil {
return fmt.Errorf("failed to get users who didn't submit: %w", err)
}

if len(users) == 0 {
log.Println("All users have submitted today!")
return nil
}

// Get today's challenge with day number
todaysChallenge, dayNumber, err := b.db.GetTodaysChallengeWithDay(today)
if err != nil {
return fmt.Errorf("failed to get today's challenge: %w", err)
}

for _, user := range users {
leetcodeProfile, err := b.db.GetLeetcodeProfile(user.ID)
if err != nil {
log.Printf("Error getting LeetCode profile for user %d: %v", user.ID, err)
continue
}

submissions, err := leetcode.GetRecentACByUsername(leetcodeProfile.Username)
done := false
for _, submission := range submissions {
if submission.Title == todaysChallenge.Title && submission.Timestamp.Format("2006-01-02") == today {
done = true
break
}
}

if done {
// Announce submission in group
var mention string
if user.Username != "" {
mention = "@" + user.Username
} else {
mention = user.FirstName
}

messageText := fmt.Sprintf("🎉 %s has just submitted today's challenge (Day %d):\n\n", mention, dayNumber)

b.sendMessage(b.config.TelegramGroupID, messageText)
}
}
return nil
}

func (b *Bot) handleRegisterLeetcodeProfile(message *tgbotapi.Message) error {
userID := message.From.ID
username := strings.TrimSpace(message.CommandArguments())
if username == "" {
b.sendMessage(message.Chat.ID, "❌ Please provide your LeetCode username. Usage: /register <leetcode_username>")
return nil
}

// Check if user is already registered
existingProfile, err := b.db.GetLeetcodeProfile(userID)
if err != nil && existingProfile != nil {
b.sendMessage(message.Chat.ID, "❌ You have already registered your LeetCode username.")
return nil
}

err = b.db.RegisterLeetcodeProfile(userID, username)
if err != nil {
log.Printf("Error registering LeetCode profile: %v", err)
b.sendMessage(message.Chat.ID, "❌ An error occurred while registering your LeetCode username.")
return err
}
b.sendMessage(message.Chat.ID, fmt.Sprintf("✅ Successfully registered your LeetCode username: %s", username))
return nil
}
38 changes: 38 additions & 0 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,13 @@ func (db *DB) createTables() error {
day_number INTEGER NOT NULL,
FOREIGN KEY (problem_id) REFERENCES problems (id)
)`,
`CREATE TABLE IF NOT EXISTS user_leetcode_profiles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL,
leetcode_username TEXT NOT NULL UNIQUE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
FOREIGN KEY (user_id) REFERENCES users (id)
)`,
`CREATE TABLE IF NOT EXISTS challenge_counter (
id INTEGER PRIMARY KEY CHECK (id = 1),
current_day INTEGER NOT NULL DEFAULT 9,
Expand Down Expand Up @@ -320,3 +327,34 @@ func (db *DB) LoadProblemsFromYAML(problems models.ProblemsData) error {

return tx.Commit()
}

// GetLeetcodeProfile gets leetcode profile of user by id
func (db *DB) GetLeetcodeProfile(id int64) (*models.User, error) {
query := `SELECT u.id, u.leetcode_username
FROM user_leetcode_profiles u
WHERE u.id = ?`

rows, err := db.conn.Query(query, id)
if err != nil {
return nil, err
}
defer rows.Close()

if rows.Next() {
var user models.User
err := rows.Scan(&user.ID, &user.Username, &user.FirstName, &user.LastName)
if err != nil {
return nil, err
}
return &user, nil
}
return nil, fmt.Errorf("no leetcode profile found for user with id %d", id)
}

// RegisterLeetcodeProfile registers a leetcode profile for a user
func (db *DB) RegisterLeetcodeProfile(userID int64, leetcodeUsername string) error {
query := `INSERT OR REPLACE INTO user_leetcode_profiles (user_id, leetcode_username, created_at)
VALUES (?, ?, COALESCE((SELECT created_at FROM user_leetcode_profiles WHERE user_id = ?), CURRENT_TIMESTAMP))`
_, err := db.conn.Exec(query, userID, leetcodeUsername, userID)
return err
}
73 changes: 73 additions & 0 deletions internal/leetcode/leetcode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package leetcode

import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)

type RecentACEntry struct {
Title string `json:"title"`
TitleSlug string `json:"titleSlug"`
Timestamp string `json:"timestamp"`
}

type RecentACData struct {
RecentACSubmissionList []RecentACEntry `json:"recentAcSubmissionList"`
}

type RecentACListResponse struct {
Data RecentACData `json:"data"`
}

type RecentAC struct {
Title string
TitleSlug string
Timestamp time.Time
}

func queryRecentACList(username string) ([]RecentACEntry, error) {
// Call leetcode graphql api
query := fmt.Sprintf(`{"query":"\n query recentAcSubmissions($username: String!, $limit: Int!) {\n recentAcSubmissionList(username: $username, limit: $limit) {\n id\n title\n titleSlug\n timestamp\n }\n}\n ","variables":{"username":"%s","limit":15},"operationName":"recentAcSubmissions"}`, username)
resp, err := http.Post("https://leetcode.com/graphql", "application/json", strings.NewReader(query))
if err != nil {
return nil, fmt.Errorf("failed to fetch recent AC submissions: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("failed to fetch recent AC submissions: %s", resp.Status)
}
var result RecentACListResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to decode response: %w", err)
}
return result.Data.RecentACSubmissionList, nil
}

func GetRecentACByUsername(username string) ([]RecentAC, error) {
if username == "" {
return nil, fmt.Errorf("username cannot be empty")
}
recentACList, err := queryRecentACList(username)
if err != nil {
return nil, fmt.Errorf("failed to get recent AC submissions for user %s: %w", username, err)
}
var result []RecentAC
for _, ac := range recentACList {
i, err := strconv.ParseInt(ac.Timestamp, 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse timestamp %s: %w", ac.Timestamp, err)
}
timestamp := time.Unix(i, 0)
result = append(result, RecentAC{
Title: ac.Title,
TitleSlug: ac.TitleSlug,
Timestamp: timestamp,
})
}
return result, nil
}

8 changes: 8 additions & 0 deletions internal/models/models.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ type User struct {
CreatedAt time.Time `json:"created_at" db:"created_at"`
}

// UserLeetcodeProfile represents a user's LeetCode profile
type UserLeetcodeProfile struct {
ID int64 `json:"id" db:"id"`
UserId int64 `json:"user_id" db:"user_id"`
LeetCodeUsername string `json:"leetcode_username" db:"leetcode_username"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
}

// Submission represents a user's submission for a daily challenge
type Submission struct {
ID int `json:"id" db:"id"`
Expand Down
11 changes: 11 additions & 0 deletions internal/scheduler/scheduler.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,17 @@ func (s *Scheduler) Start() {
log.Printf("Error scheduling evening reminder: %v", err)
}

// Schedule check submissions every 5 minutes
_, err = s.cron.AddFunc("*/5 * * * *", func() {
log.Println("Checking new submissions...")
if err := s.bot.CheckSubmissions(); err != nil {
log.Printf("Error while checking new submissions %v", err)
}
})
if err != nil {
log.Printf("Error scheduling check new submissions: %v", err)
}

// Start the cron scheduler
s.cron.Start()
log.Println("Scheduler started successfully - posting challenges Monday to Friday only")
Expand Down
Loading