Dockerfile
Signed-off-by: adithyagenie <adithyagenie@gmail.com> Update .woodpecker/woodpecker.yml
This commit is contained in:
parent
01df8c87d7
commit
a5d82b03e7
16 changed files with 651 additions and 1316 deletions
13
.woodpecker/woodpecker.yml
Normal file
13
.woodpecker/woodpecker.yml
Normal 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
37
Dockerfile
Normal 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"]
|
|
@ -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
8
ecosystem.config.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module.exports = [
|
||||||
|
{
|
||||||
|
script: 'out/index.js',
|
||||||
|
name: 'forceplusplus',
|
||||||
|
exec_mode: 'cluster',
|
||||||
|
instances: 1
|
||||||
|
}
|
||||||
|
]
|
|
@ -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,
|
|
||||||
);
|
|
21
package.json
21
package.json
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1741
pnpm-lock.yaml
1741
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -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 :(");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 :(");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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("*********************");
|
||||||
|
|
|
@ -13,7 +13,7 @@ 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"
|
||||||
};
|
};
|
||||||
|
|
|
@ -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.`)
|
||||||
|
|
|
@ -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));
|
||||||
|
|
||||||
|
|
14
src/index.ts
14
src/index.ts
|
@ -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) => {
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in a new issue