Dockerfile

Signed-off-by: adithyagenie <adithyagenie@gmail.com>

Update .woodpecker/woodpecker.yml
This commit is contained in:
adithyagenie 2024-06-16 00:04:26 +05:30
parent 01df8c87d7
commit a5d82b03e7
Signed by: adithyagenie
GPG key ID: C66E41599E458D96
16 changed files with 651 additions and 1316 deletions

View file

@ -0,0 +1,13 @@
when:
- event: push
steps:
- name: Build & Push
image: git.ptr.moe/baalajimaestro/build-runner
commands:
- /dockerd-entrypoint.sh dockerd 2&> /dev/null &
- echo $DOCKER_PASSWORD | docker login git.ptr.moe --username ${CI_REPO_OWNER} --password-stdin
- docker build . -t git.ptr.moe/adithyagenie/forceplusplus:latest
- docker push git.ptr.moe/adithyagenie/forceplusplus:latest
secrets: [ docker_password ]
privileged: true

37
Dockerfile Normal file
View file

@ -0,0 +1,37 @@
FROM node:20-alpine as build
RUN npm install pnpm -g
RUN mkdir /app && \
chown -R node:node /app
USER node
WORKDIR /app
ADD package.json .
ADD pnpm-lock.yaml .
RUN pnpm install --frozen-lockfile
ADD src ./src
ADD tsconfig.json .
ADD ecosystem.config.js .
RUN pnpm run build
FROM node:20-alpine
RUN mkdir /app && \
chown -R node:node /app
RUN npm install pm2 -g
USER node
WORKDIR /app
COPY --from=build /app/out ./out/
COPY --from=build /app/node_modules ./node_modules/
COPY --from=build /app/ecosystem.config.js ./ecosystem.config.js
CMD ["pm2-runtime", "ecosystem.config.js"]

View file

@ -1,10 +1,13 @@
# Force++ (F++) # Force++ (F++)
Force++ (F++ for short) is a Telegram bot designed to notify users about upcoming coding contests on popular competitive programming platforms. Currently, the bot supports notifications for LeetCode, Codeforces and CodeChef contests. Force++ (F++ for short) is a Telegram bot designed to notify users about upcoming coding contests on popular competitive
programming platforms. Currently, the bot supports notifications for LeetCode, Codeforces and CodeChef contests.
## ⚠️ Development Status ## ⚠️ Development Status
**Note: This bot is heavily unstable and highly in development.** Use it at your own risk, and expect frequent changes and potential issues. As of now, it works for a single group or person configured by the chat ID in environment variables. **Note: This bot is heavily unstable and highly in development.** Use it at your own risk, and expect frequent changes
and potential issues. As of now, it works for a single group or person configured by the chat ID in environment
variables.
## Installation ## Installation

8
ecosystem.config.js Normal file
View file

@ -0,0 +1,8 @@
module.exports = [
{
script: 'out/index.js',
name: 'forceplusplus',
exec_mode: 'cluster',
instances: 1
}
]

View file

@ -1,9 +0,0 @@
// @ts-check
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
);

View file

@ -6,29 +6,26 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"start": "node ./out/index.js", "start": "node ./out/index.js",
"dev": "node --watch ./out/index.js" "dev": "node --watch ./out/index.js",
"test": "xo"
}, },
"keywords": [], "keywords": [],
"author": "", "author": "",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
"grammy": "^1.24.1",
"dotenv": "^16.4.5",
"node-schedule": "^2.1.1",
"@grammyjs/parse-mode": "^1.10.0", "@grammyjs/parse-mode": "^1.10.0",
"codeforces-api-ts": "^3.0.1",
"dotenv": "^16.4.5",
"fastify": "^4.27.0", "fastify": "^4.27.0",
"grammy": "^1.24.1",
"leetcode-query": "^1.2.3", "leetcode-query": "^1.2.3",
"codeforces-api-ts": "^3.0.1" "node-schedule": "^2.1.1"
}, },
"devDependencies": { "devDependencies": {
"@grammyjs/types": "^3.8.0", "@grammyjs/types": "^3.8.0",
"typescript": "^5.4.5",
"@types/node-schedule": "^2.1.7",
"eslint": "^9.4.0",
"typescript-eslint": "^7.12.0",
"@eslint/js": "^9.4.0",
"@types/eslint__js": "^8.42.3",
"@types/node": "^20.14.2", "@types/node": "^20.14.2",
"@types/node-fetch": "^2.6.11" "@types/node-fetch": "^2.6.11",
"@types/node-schedule": "^2.1.7",
"typescript": "^5.4.5"
} }
} }

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,7 @@ export async function cfFetchContests(): Promise<{ name: string; startTime: numb
id: o.id, id: o.id,
startTime: o.startTimeSeconds as number startTime: o.startTimeSeconds as number
})); }));
} } else {
else {
throw new Error("Unable to reach Codeforces API :("); throw new Error("Unable to reach Codeforces API :(");
} }
} }

View file

@ -30,12 +30,11 @@ export async function fetchLCContests() {
if (resp.ok) { if (resp.ok) {
const res = await resp.json() as PastContestsResponse; const res = await resp.json() as PastContestsResponse;
let contestNames = res.data.pastContests.data.map(o => { let contestNames = res.data.pastContests.data.map(o => {
return {key: o.titleSlug, name: o.title}; return { key: o.titleSlug, name: o.title };
}); });
contestNames.sort(); contestNames.sort();
return contestNames; return contestNames;
} } else {
else {
throw new Error("Can't access LeetCode API :("); throw new Error("Can't access LeetCode API :(");
} }
} }

View file

@ -9,7 +9,7 @@ export async function startserver() {
const server = fastify({ logger: false }); const server = fastify({ logger: false });
if (process.env.RUN_METHOD === "WEBHOOK") { if (process.env.RUN_METHOD === "WEBHOOK") {
server.post(`/`, webhookCallback(bot, "fastify", {secretToken: process.env.WEBHOOK_SECRET as string})); server.post(`/`, webhookCallback(bot, "fastify", { secretToken: process.env.WEBHOOK_SECRET as string }));
server.setErrorHandler(async(err, req, res) => { server.setErrorHandler(async(err, req, res) => {
console.error(`Encountered error: ${err.name}\nStack trace: ${err.stack}`); console.error(`Encountered error: ${err.name}\nStack trace: ${err.stack}`);
await res.status(200).send({}); await res.status(200).send({});
@ -18,10 +18,10 @@ export async function startserver() {
server.get("/", async() => { server.get("/", async() => {
return "Force++ bot up and running ^_^"; return "Force++ bot up and running ^_^";
}); });
await server.listen({ port: port }); await server.listen({ port: port, host: "0.0.0.0" });
console.log("Server up!") console.log("Server up!")
if (process.env.RUN_METHOD === "WEBHOOK") if (process.env.RUN_METHOD === "WEBHOOK")
await bot.api.setWebhook(`${process.env.WEBHOOK_URL}`, {secret_token: process.env.WEBHOOK_SECRET as string}); await bot.api.setWebhook(`${process.env.WEBHOOK_URL}`, { secret_token: process.env.WEBHOOK_SECRET as string });
console.log(`Force++ server listening on port ${port}!`); console.log(`Force++ server listening on port ${port}!`);
return server; return server;

View file

@ -11,7 +11,7 @@ export async function botInit() {
setCommands(bot); setCommands(bot);
bot.hears(/^\/start/, async(ctx) => await startCommand(ctx)); bot.hears(/^\/start/, async(ctx) => await startCommand(ctx));
bot.hears(/^\/help/, async(ctx) => await helpCommand(ctx)); bot.hears(/^\/help/, async(ctx) => await helpCommand(ctx));
bot.hears(/.+/, async(ctx) => await messageSink(ctx)); bot.chatType("private").hears(/.+/, async(ctx) => await messageSink(ctx));
console.log("*********************"); console.log("*********************");
console.log("Force++ has started!"); console.log("Force++ has started!");
console.log("*********************"); console.log("*********************");

View file

@ -13,10 +13,10 @@ Attend the contest here: https://www.codechef.com/contests/`);
} }
export async function reminderContestCF(contestName: string, contestID: number, contestTime: Date) { export async function reminderContestCF(contestName: string, contestID: number, contestTime: Date) {
let options: {timeZone: string, timeStyle: "short"} = { let options: { timeZone: string, timeStyle: "short" } = {
timeZone: 'Asia/Calcutta', timeZone: 'Asia/Calcutta',
timeStyle: "short" timeStyle: "short"
}; };
await bot.api.sendMessage(parseInt(process.env.CHAT_ID as string), await bot.api.sendMessage(parseInt(process.env.CHAT_ID as string),
`${contestName} at ${new Date(contestTime).toLocaleTimeString('en-US', options)} IST starts in ~10 minutes. `${contestName} at ${new Date(contestTime).toLocaleTimeString('en-US', options)} IST starts in ~10 minutes.
Attend the contest here: https://codeforces.com/contests/${contestID}`); Attend the contest here: https://codeforces.com/contests/${contestID}`);

View file

@ -1,4 +1,4 @@
import { Bot, Context } from "grammy"; import { Context } from "grammy";
import { myBot } from "../bot"; import { myBot } from "../bot";
export function setCommands(bot: myBot) { export function setCommands(bot: myBot) {
@ -11,12 +11,14 @@ export function setCommands(bot: myBot) {
export async function startCommand(ctx: Context) { export async function startCommand(ctx: Context) {
await ctx.reply(` await ctx.reply(`
Hello! 👋 I'm Force++, your coding contest notification bot. Hello! I'm Force++, your coding contest notification bot.
🔔 Currently, I'm not open for public use. Stay tuned for future updates.`);
🔔 Currently, I'm not open for public use. Stay tuned for future updates.`);
} }
export async function helpCommand(ctx: Context) { export async function helpCommand(ctx: Context) {
await ctx.reply(`I'm Force++ (F++), a bot designed to give reminders for contests over different competitive coding platforms. await ctx.reply(`
I'm Force++ (F++), a bot designed to give reminders for contests over different competitive coding platforms.
I'm not <b>available for public use</b> and currently under development. Check out my source code over <a href="https://git.ptr.moe/adithyagenie/forceplusplus">here</a>. I'm not <b>available for public use</b> and currently under development. Check out my source code over <a href="https://git.ptr.moe/adithyagenie/forceplusplus">here</a>.
Designed by @adithyagenie.`) Designed by @adithyagenie.`)

View file

@ -1,4 +1,4 @@
import { gracefulShutdown, RecurrenceRule, scheduleJob } from "node-schedule"; import { RecurrenceRule, scheduleJob } from "node-schedule";
import { reminderContestLC } from "../bot/commands/contestRem"; import { reminderContestLC } from "../bot/commands/contestRem";
import { fetchLCContests } from "../api/lcFetch"; import { fetchLCContests } from "../api/lcFetch";
@ -18,7 +18,6 @@ function getNextBiweeklyDate(): Date {
} }
export async function lcSchedule() { export async function lcSchedule() {
const contestNames = await fetchLCContests(); const contestNames = await fetchLCContests();
const biweekly = contestNames.filter(o => const biweekly = contestNames.filter(o =>
@ -29,7 +28,7 @@ export async function lcSchedule() {
); );
if (biweekly.length > 0) { if (biweekly.length > 0) {
biweekly.sort((a, b) => a > b ? - 1 : 1); biweekly.sort((a, b) => a > b ? -1 : 1);
const matches = biweekly[0].key.match(/^biweekly-contest-(\d+)$/); const matches = biweekly[0].key.match(/^biweekly-contest-(\d+)$/);
const newnum = matches ? parseInt(matches[1]) + 1 : -1; const newnum = matches ? parseInt(matches[1]) + 1 : -1;
@ -39,13 +38,13 @@ export async function lcSchedule() {
const date = getNextBiweeklyDate(); const date = getNextBiweeklyDate();
date.setMinutes(date.getMinutes() - 10); date.setMinutes(date.getMinutes() - 10);
scheduleJob(`${newname}`, date, async function (newname: string, newkey: string) { scheduleJob(`${newname}`, date, async function(newname: string, newkey: string) {
await reminderContestLC(newname, newkey) await reminderContestLC(newname, newkey)
}.bind(null, newname, newkey)); }.bind(null, newname, newkey));
} }
if (weekly.length > 0) { if (weekly.length > 0) {
weekly.sort((a, b) => a > b ? - 1 : 1); weekly.sort((a, b) => a > b ? -1 : 1);
const matches = weekly[0].key.match(/^weekly-contest-(\d+)$/); const matches = weekly[0].key.match(/^weekly-contest-(\d+)$/);
const newnum = matches ? parseInt(matches[1]) + 1 : -1; const newnum = matches ? parseInt(matches[1]) + 1 : -1;
@ -59,7 +58,7 @@ export async function lcSchedule() {
weeklyJob.second = 0; weeklyJob.second = 0;
weeklyJob.tz = "Etc/UTC"; weeklyJob.tz = "Etc/UTC";
scheduleJob(`${newname}`, weeklyJob, async function (newname: string, newkey: string) { scheduleJob(`${newname}`, weeklyJob, async function(newname: string, newkey: string) {
await reminderContestLC(newname, newkey) await reminderContestLC(newname, newkey)
}.bind(null, newname, newkey)); }.bind(null, newname, newkey));

View file

@ -1,11 +1,21 @@
import { config } from "dotenv"; import { config } from "dotenv";
config();
import { FastifyInstance } from "fastify"; import { FastifyInstance } from "fastify";
import { startserver } from "./api/server"; import { startserver } from "./api/server";
import { botInit } from "./bot/bot"; import { botInit } from "./bot/bot";
import { contestScheduler } from "./helpers/scheduler"; import { contestScheduler } from "./helpers/scheduler";
config();
if (process.env.BOT_TOKEN === undefined ||
process.env.RUN_METHOD === undefined ||
(process.env.RUN_METHOD !== "POLLING" && process.env.RUN_METHOD !== "WEBHOOK") ||
process.env.CHAT_ID === undefined ||
(process.env.RUN_METHOD === "WEBHOOK" && (process.env.WEBHOOK_URL === undefined || process.env.WEBHOOK_SECRET === undefined))
) {
console.error("ENV variables are not set correctly.");
process.exit(1);
}
export let server: FastifyInstance; export let server: FastifyInstance;
startserver().then((s) => { startserver().then((s) => {

View file

@ -1,7 +1,7 @@
{ {
"compilerOptions": { "compilerOptions": {
"outDir": "./out", "outDir": "./out",
"target": "ES2017", "target": "ES2021",
"module": "commonjs", "module": "commonjs",
"allowJs": true, "allowJs": true,
"strict": true "strict": true