the notorious
snacky

<- Quay về trang chủ

Side-project ký sự: Leetcode Slack Bot

Đáng ra thì bài tiếp theo trong sê ri Side Project Ký Sự này mình sẽ viết phần 2 về ChatUML, nhưng có nhiều vấn đề khiến mình ko thể tiếp tục share được, đành cáo lỗi với các bạn. Tóm tắt nhanh thì tổng doanh thu của dự án đến thời điểm này là 20k USD, và đã ngừng tăng trưởng . Nhưng mà thôi, mình sẽ tiếp tục viết về các side project khác.


Algorithm Bot là một con Slack bot hoạt động trong channel #algorithm ở cộng đồng WeBuild VN. Được chính các thành viên làm ra để tự động hoá việc nhắc nhở các thành viên tham gia luyện Leetcode mỗi ngày, nâng cao tay nghề, với motto là: "A leetcode a day, keep layoff away".

Nhưng vì một lý do nào đó, mà từ khi làm ra con bot này thì mình đi phỏng vấn toàn rớt.

Trong bài viết này, mình sẽ chia sẽ một tí về cách mà con bot này được làm ra. Mục đích không phải để các bạn hiểu thêm về những công nghệ tối tân đằng sau con bot, mà chỉ để khoe mẽ. Vì có thể các bạn chưa biết, một engineer giỏi không phải là người có thể build được tất cả mọi thứ, mà là một người biết cách khoe những gì mình đã build.

Ở ngoài nhìn vào thì Algorithm Slack Bot có các chức năng sau:

  1. Cho phép user đăng ký tham gia giải bài và nhận thông báo mỗi ngày
  2. Tự động gửi tin nhắn vào channel #algorithm, tag các thành viên đã đăng ký để nhắc làm bài
  3. Khi user giải xong bài trên Leetcode, thì có thể bấm vào nút "Tui giải xong rồi" trên Slack để điểm danh. Algorithm Bot sẽ kiểm tra tài khoản Leetcode của user để xem có đúng là đã giải xong bài chưa, nếu chưa thì sẽ bị nó mắng.

Bạn nào giải bài và điểm danh xong thì sẽ bị khen thưởng:

Còn những ai lề mề chậm trễ, chờ tới cuối ngày mới điểm danh thì sẽ được phạt:

Ở trong nhìn ra, thì Algorithm Bot thực chất chỉ là một file TypeScript 800 dòng, chạy trên Deno Deploy. Toàn bộ dữ liệu chỉ là 1 cục JS object lưu trong Deno KV. Ngoài mình ra thì còn có hai bạn khác là Tuan Cá Thu (Tuna)Huấn Pa Tê (Pêtr) tham gia phát triển.

Mô hình hoạt động của con bot có thể được tóm tắt bằng sơ đồ bên dưới:

Chúng ta có 3 thành phần chính, handle 3 flow khác nhau:

Flow đăng ký: Để Algorithm Bot có thể nhận diện được mình là ai ở trên Slack và trên Leetcode, thì bạn có thể gửi Slack command là /enroll <leetcode-id>, khi gặp lệnh này, Slack sẽ gửi một HTTP request đến Algorithm Bot server:

POST /enroll
{
    "user_id": "<SLACK ID>",
    "text": "<LEETCODE ID>"
}

phía server, chúng ta đọc dữ liệu từ Deno KV để lấy ra bảng tham chiếu [Slack ID : Leetcode ID], rồi lưu hoặc update Leetcode ID mới nếu cần. Nếu bạn đổi account Leetcode thì chỉ cần chạy lại lệnh enroll là được.

Tương tự với flow đăng ký nhận thông báo khi có bài mới được post, ở đây chúng ta dùng lệnh /subscribe hoặc /unsubscribe, các bạn có thể tham khảo source để xem cách implement.

Cron job chạy mỗi ngày vào lúc 00:05 UTC, tương đương với 7:05 sáng giờ VN, 5 phút sau khi Leetcode mở bài daily. Khi chạy, Algorithm Bot sẽ gửi một GraphQL request với query questionOfToday đến Leetcode, lấy thông tin về bài daily, sau đó soạn một Slack message có nội dung giống như screenshot ở trên, danh sách những ai đăng ký nhận thông báo được lưu ở flow phía trên cũng sẽ được lấy ra và tag vào message.

// Setup cron job
Deno.cron("Send daily challenge", "5 0 * * *", async () => {
    await sendDailyProblem();
});

// Gửi tin
async sendDailyProblem() {
    const question = await getDailyQuestion();
    prepareAndSendSlackMessage(question);
}

// Lấy bài từ Leetcode
async function getDailyQuestion(): Promise<DailyQuestion> {
    const query = `
    query questionOfToday {
        activeDailyCodingChallengeQuestion {
            date
            userStatus
            link
            question {
                questionId
                questionFrontendId
                title
                titleSlug
                acRate
                difficulty
            }
        }}
        `;

    const result = await callGraphQL(query, 'questionOfToday');
    return result.data.activeDailyCodingChallengeQuestion;
}

Nút bấm để điểm danh cũng sẽ được tạo, và đính kèm với ID của câu hỏi daily, khi các bạn điểm danh thì server sẽ biết được các bạn đang điểm danh cho bài nào.

{
    "type": "actions",
    "elements": [
        {
            "type": "button",
            "text": {
                "type": "plain_text",
                "text": "✅ Tui giải xong rồi!",
                "emoji": true
            },
            "value": `[dl]${questionTitleSlug}`,
            "action_id": "problem_solved"
        }
    ]
}

Cuối cùng là flow xác thực việc điểm danh. Khi nhấn nút điểm danh, một HTTP request khác sẽ được gửi đến Algorithm Bot server, kèm với đó là thông tin người điểm danh. Dựa vào data đã lưu, chúng ta xác định được Leetcode ID, gửi GraphQL request với query getACSubmissions, lấy ra 5 submission gần nhất để xem bài đang giải có nằm trong đó không. Nếu có thì gửi tin nhắn thông báo vào thread trên Slack, rồi lưu kết quả điểm danh vào DB.

async function verifySubmission(payload) {
    const leetcodeAccountMap = await getLeetcodeAccountMap(denoKV);
    const userId = payload.user.id;
    const problemSlug = readProblemSlug(payload);

    ensureUserHasLeetcodeAccount();

    const query = `
        query getACSubmissions ($username: String!, $limit: Int) {
            recentAcSubmissionList(username: $username, limit: $limit) {
                title
                titleSlug
                timestamp
                statusDisplay
                lang
            }
        }
    `;
    const recentSubmission = await callGraphQL(acQuery, 'getACSubmissions', {
        username: leetcodeAccountMap[userId],
        limit: 5
    });
    const userSubmitted = recentSubmission.find(sb => sb.titleSlug == problemSlug);

    rankingAndSendResponse();
}

Việc xác định thứ hạn khi điểm danh dựa vào hai yếu tố:

Ngoài ra Algorithm Bot còn có chức năng tạo challenge phụ, bằng cách paste link của bài Leetcode vào, cách hoạt động tương tự như trên. Nhưng vì vẽ xong sơ đồ ở trên rồi mình mới nhớ ra, nên nếu các bạn nhìn vào sơ đồ thì sẽ thấy mình không nhắc đến chức năng này


Nói sơ một chút về lý do tại sao chọn TypeScript, Deno và Deno Deploy, trong khi có nhiều ngôn ngữ cool ngầu hơn ở ngoài kia? Sau khi xác định được sẽ build cái gì, thì mình nhận thấy con Slack bot cần được deploy lên một nơi có những thứ sau:

Đến khúc này thì có rất nhiều giải pháp:

Mình có thể thuê một con VPS và có thể setup tất cả mọi thứ, từ DB đến cron job, nhưng mặc dù mình là một người hết mình vì cộng động, không đời nào mình chịu bỏ ra 5 đô một tháng để thuê server hết

OK, không dùng server riêng thì setup một cái HTTP server trên Fly.io hoặc Vercel, Netlify, xong rồi dùng Github Action hoặc Vercel CRON để chạy task cũng được, rồi lại phải tìm một service khác để handle DB. Như vậy để run một cái app nhỏ, cần dùng đến 3, 4 service khác nhau, rồi về sau ai maintain cái đống đó? mình thích những thứ gì nó tinh gọn, tốt nhất là tất cả nên nằm cùng 1 chỗ.

Vậy nên sau đó thì mình tình cờ tìm ra Deno Deploy, và thấy nó tick đủ hết các yếu tố mình cần: built-in database, có, support cron job, có, dễ dàng deploy, có, có thể manage tất cả service ở cùng 1 chỗ, có, miễn phí, có luôn, tác giả blog này đẹp trai tài giỏi, có luôn.

Chỉ có một vấn đề duy nhất đó là Deno Deploy chỉ hỗ trợ Deno/TypeScript, trong khi dự định ban đầu mình muốn xài Rust hoặc Java, nhưng mà không sao, chuyện gì cũng phải có trade off cả.

Rồi xong, giờ thì các bạn còn chờ gì nữa, join WeBuild và giải algo với tụi mình nào.