IGNOU Telegram Bot v2.0
This commit is contained in:
commit
9668553828
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
__pycache__
|
||||||
|
*.session
|
65
bot/__main__.py
Normal file
65
bot/__main__.py
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
from pyrogram import Client, filters
|
||||||
|
from pyrogram.enums import ChatType, ParseMode
|
||||||
|
|
||||||
|
from .ignou import IgnouResult, ResultNotFoundError
|
||||||
|
|
||||||
|
try:
|
||||||
|
BOT_TOKEN = os.environ["BOT_TOKEN"]
|
||||||
|
except KeyError:
|
||||||
|
print("BOT_TOKEN not found in environment variables.")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
app = Client(
|
||||||
|
":ignou:",
|
||||||
|
api_id=os.environ.get("API_ID", "21724"),
|
||||||
|
api_hash=os.environ.get("API_HASH", "3e0cb5efcd52300aec5994fdfc5bdc16"),
|
||||||
|
bot_token=BOT_TOKEN,
|
||||||
|
in_memory=True
|
||||||
|
)
|
||||||
|
|
||||||
|
ignou = IgnouResult()
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_message(filters.command("start"))
|
||||||
|
async def start(_, message):
|
||||||
|
await message.reply_text(
|
||||||
|
f"Hi {message.from_user.mention}!\nSend me your program code with enrollment number to get your grade card result.",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.on_message(filters.regex(r"([a-zA-z]+)\s*?(\d+$)"))
|
||||||
|
async def get_grade_result(_, message):
|
||||||
|
|
||||||
|
if message.chat.type == ChatType.GROUP or message.chat.type == ChatType.SUPERGROUP:
|
||||||
|
await message.delete()
|
||||||
|
|
||||||
|
program_id, enrollment_no = message.matches[0].groups()
|
||||||
|
|
||||||
|
try:
|
||||||
|
footer = f"<strong>Result checked by {message.from_user.mention(message.from_user.first_name)}</strong>"
|
||||||
|
except AttributeError:
|
||||||
|
footer = ""
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await ignou.gradeResultData(program_id, enrollment_no)
|
||||||
|
except ResultNotFoundError:
|
||||||
|
await message.reply_text("Grade Card Result not found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
if message.chat.type == ChatType.GROUP or message.chat.type == ChatType.SUPERGROUP:
|
||||||
|
await message.reply_text(
|
||||||
|
f"<pre>{result['table']}</pre>\n{footer}",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
)
|
||||||
|
elif message.chat.type == ChatType.PRIVATE:
|
||||||
|
await message.reply_text(
|
||||||
|
f"<pre>{result['header']}{result['table']}</pre>",
|
||||||
|
parse_mode=ParseMode.HTML,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
app.run()
|
224
bot/ignou.py
Normal file
224
bot/ignou.py
Normal file
|
@ -0,0 +1,224 @@
|
||||||
|
from collections import OrderedDict
|
||||||
|
from collections.abc import Mapping
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
|
from prettytable import PrettyTable
|
||||||
|
|
||||||
|
|
||||||
|
class DotDict(OrderedDict):
|
||||||
|
"""
|
||||||
|
Quick and dirty implementation of a dot-able dict, which allows access and
|
||||||
|
assignment via object properties rather than dict indexing.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# we could just call super(DotDict, self).__init__(*args, **kwargs)
|
||||||
|
# but that won't get us nested dotdict objects
|
||||||
|
od = OrderedDict(*args, **kwargs)
|
||||||
|
for key, val in od.items():
|
||||||
|
if isinstance(val, Mapping):
|
||||||
|
value = DotDict(val)
|
||||||
|
else:
|
||||||
|
value = val
|
||||||
|
self[key] = value
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
return self.get(k, "-")
|
||||||
|
|
||||||
|
__setattr__ = OrderedDict.__setitem__
|
||||||
|
|
||||||
|
|
||||||
|
class ResultNotFoundError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class IgnouResult:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
transport = httpx.AsyncHTTPTransport(verify=False)
|
||||||
|
self.session: httpx.AsyncClient = httpx.AsyncClient(transport=transport)
|
||||||
|
|
||||||
|
async def get_grade_result(self, program_id: str, enrollment_no: int):
|
||||||
|
|
||||||
|
program_id = program_id.upper()
|
||||||
|
|
||||||
|
params = {
|
||||||
|
"prog": program_id,
|
||||||
|
"eno": enrollment_no,
|
||||||
|
}
|
||||||
|
|
||||||
|
if program_id in [
|
||||||
|
"BCA",
|
||||||
|
"MCA",
|
||||||
|
"MP",
|
||||||
|
"MBP",
|
||||||
|
"PGDHRM",
|
||||||
|
"PGDFM",
|
||||||
|
"PGDOM",
|
||||||
|
"PGDMM",
|
||||||
|
"PGDFMP",
|
||||||
|
]:
|
||||||
|
params["type"] = 1
|
||||||
|
elif program_id in ["ASSSO", "BA", "BCOM", "BDP", "BSC"]:
|
||||||
|
params["type"] = 2
|
||||||
|
elif program_id in [
|
||||||
|
"BAEGH",
|
||||||
|
"BAG",
|
||||||
|
"BAHDH",
|
||||||
|
"BAHIH",
|
||||||
|
"BAPAH",
|
||||||
|
"BAPCH",
|
||||||
|
"BAPSH",
|
||||||
|
"BASOH",
|
||||||
|
"BAVTM",
|
||||||
|
"BCOMG",
|
||||||
|
"BSCANH",
|
||||||
|
"BSCBCH",
|
||||||
|
"BSCG",
|
||||||
|
"BSWG",
|
||||||
|
]:
|
||||||
|
params["type"] = 4
|
||||||
|
else:
|
||||||
|
params["type"] = 3
|
||||||
|
|
||||||
|
grade_card_page = "https://gradecard.ignou.ac.in/gradecard/view_gradecard.aspx"
|
||||||
|
|
||||||
|
response = await self.session.get(grade_card_page, params=params)
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, "lxml")
|
||||||
|
|
||||||
|
name = soup.find(id="ctl00_ContentPlaceHolder1_lblDispname").string
|
||||||
|
|
||||||
|
try:
|
||||||
|
trs = soup.find(string="COURSE").findParent("table")
|
||||||
|
except AttributeError:
|
||||||
|
raise ResultNotFoundError
|
||||||
|
|
||||||
|
trs = soup.find(string="COURSE").findParent("table").find_all("tr")
|
||||||
|
|
||||||
|
data = DotDict()
|
||||||
|
data.enrollment_no = enrollment_no
|
||||||
|
data.program_id = program_id
|
||||||
|
data.name = name
|
||||||
|
|
||||||
|
courses = []
|
||||||
|
data.courses = courses
|
||||||
|
|
||||||
|
total = DotDict()
|
||||||
|
data.total = total
|
||||||
|
total.total_passed = 0
|
||||||
|
total.total_failed = 0
|
||||||
|
total.total_marks = 0
|
||||||
|
total.total_obtained_marks = 0
|
||||||
|
|
||||||
|
column_index = {}
|
||||||
|
# get index of coulmn by name
|
||||||
|
|
||||||
|
cols_list = [data.get_text() for data in trs[0].find_all("th")]
|
||||||
|
for index, col in enumerate(cols_list):
|
||||||
|
if "COURSE" in col:
|
||||||
|
column_index["COURSE"] = index
|
||||||
|
elif "ASG" in col.upper():
|
||||||
|
column_index["ASIGN"] = index
|
||||||
|
elif "THEORY" in col:
|
||||||
|
column_index["THEORY"] = index
|
||||||
|
elif "PRACTICAL" in col:
|
||||||
|
column_index["PRACTICAL"] = index
|
||||||
|
elif "STATUS" in col:
|
||||||
|
column_index["STATUS"] = index
|
||||||
|
|
||||||
|
for tr in trs[1:-1]:
|
||||||
|
course = DotDict()
|
||||||
|
course.max_marks = 100
|
||||||
|
total.total_marks += course.max_marks
|
||||||
|
|
||||||
|
td = tr.find_all("td")
|
||||||
|
|
||||||
|
# course name
|
||||||
|
course.name = td[column_index["COURSE"]].string.strip()
|
||||||
|
|
||||||
|
# assignments marks
|
||||||
|
if assign := td[column_index["ASIGN"]].string.strip():
|
||||||
|
if assign.isnumeric():
|
||||||
|
course.assignment_marks = int(assign)
|
||||||
|
total.total_obtained_marks += course.assignment_marks
|
||||||
|
|
||||||
|
# theory marks
|
||||||
|
try:
|
||||||
|
if theory := td[column_index["THEORY"]].string.strip():
|
||||||
|
if theory.isnumeric():
|
||||||
|
course.theory_marks = int(theory)
|
||||||
|
total.total_obtained_marks += course.theory_marks
|
||||||
|
except Exception:
|
||||||
|
course.theory_marks = "-"
|
||||||
|
|
||||||
|
# lab marks
|
||||||
|
try:
|
||||||
|
if lab := td[column_index["PRACTICAL"]].string.strip():
|
||||||
|
if lab.isnumeric():
|
||||||
|
course.lab_marks = int(lab)
|
||||||
|
total.total_obtained_marks += course.lab_marks
|
||||||
|
except Exception:
|
||||||
|
course.lab_marks = "-"
|
||||||
|
|
||||||
|
# Status # ✅ ❌
|
||||||
|
if "NOT" not in td[column_index["STATUS"]].string.strip():
|
||||||
|
course.status = True
|
||||||
|
total.total_passed += 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
course.status = False
|
||||||
|
total.total_failed += 1
|
||||||
|
|
||||||
|
courses.append(course)
|
||||||
|
|
||||||
|
total.total_courses = len(courses)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"enrollment_no": enrollment_no,
|
||||||
|
"program_id": program_id,
|
||||||
|
"courses": courses,
|
||||||
|
**total,
|
||||||
|
}
|
||||||
|
|
||||||
|
async def gradeResultData(
|
||||||
|
self,
|
||||||
|
program_id: str,
|
||||||
|
enrollment_no: int,
|
||||||
|
):
|
||||||
|
|
||||||
|
data = DotDict(await self.get_grade_result(program_id, enrollment_no))
|
||||||
|
|
||||||
|
x = PrettyTable()
|
||||||
|
x.padding_width = 0
|
||||||
|
x.field_names = ["Course", "Asign", "Lab", "Term", "Status"]
|
||||||
|
|
||||||
|
header = "Name : {}\nProg : {} [{}]\n".format(
|
||||||
|
data.name, data.program_id, data.enrollment_no
|
||||||
|
)
|
||||||
|
|
||||||
|
for course in data.courses:
|
||||||
|
tick = "✅" if course.status else "❌"
|
||||||
|
|
||||||
|
x.add_row(
|
||||||
|
[
|
||||||
|
course.name,
|
||||||
|
course.assignment_marks,
|
||||||
|
course.lab_marks,
|
||||||
|
course.theory_marks,
|
||||||
|
tick,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
x.add_row(
|
||||||
|
[
|
||||||
|
"Total",
|
||||||
|
"T:{}".format(data.total_courses),
|
||||||
|
"-", # data.total_obtained_marks,
|
||||||
|
"-", # data.total_marks,
|
||||||
|
f"[{data.total_passed}/{data.total_failed}]",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"header": header, "table": x.get_string()}
|
44
flake.lock
Normal file
44
flake.lock
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1652776076,
|
||||||
|
"narHash": "sha256-gzTw/v1vj4dOVbpBSJX4J0DwUR6LIyXo7/SuuTJp1kM=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "04c1b180862888302ddfb2e3ad9eaa63afc60cf8",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"ref": "v1.0.0",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1675757258,
|
||||||
|
"narHash": "sha256-pIRer8vdsoherlRKpzfnHbMZ5TsAcvRlXHCIaHkIUbg=",
|
||||||
|
"owner": "NixOs",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "af96094e9b8eb162d70a84fa3b39f4b7a8b264d2",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOs",
|
||||||
|
"ref": "nixos-22.11",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
50
flake.nix
Normal file
50
flake.nix
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
{
|
||||||
|
description = "IGNOU Telegram Bot v2.0";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOs/nixpkgs/nixos-22.11";
|
||||||
|
inputs.flake-utils.url = "github:numtide/flake-utils/v1.0.0";
|
||||||
|
|
||||||
|
outputs = inputs: inputs.flake-utils.lib.eachDefaultSystem ( system :
|
||||||
|
let
|
||||||
|
pname = "ignou";
|
||||||
|
version = "2.0";
|
||||||
|
|
||||||
|
pkgs = inputs.nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
pyEnv = pkgs.python3.withPackages (p: with p; [
|
||||||
|
pyrogram
|
||||||
|
tgcrypto
|
||||||
|
httpx
|
||||||
|
prettytable
|
||||||
|
beautifulsoup4
|
||||||
|
]);
|
||||||
|
|
||||||
|
ignouDrv = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "ignou-telegram-bot";
|
||||||
|
version = "2.0";
|
||||||
|
src = ./.;
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bot
|
||||||
|
cp -r bot/* $out/bot/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
|
||||||
|
ignouScript = pkgs.writeShellScriptBin "start-bot" ''
|
||||||
|
cd ${ignouDrv}
|
||||||
|
${pyEnv}/bin/python3 -m bot'';
|
||||||
|
|
||||||
|
in {
|
||||||
|
|
||||||
|
packages.deafult = pkgs.buildEnv {
|
||||||
|
name = "${pname}-${version}";
|
||||||
|
paths = [ ignouDrv pyEnv ];
|
||||||
|
};
|
||||||
|
|
||||||
|
devShell = pkgs.mkShell {
|
||||||
|
buildInputs = [ pyEnv ];
|
||||||
|
};
|
||||||
|
|
||||||
|
nixosModules.default = import ./nix/module.nix inputs;
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
65
flake.nix.bak
Normal file
65
flake.nix.bak
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{
|
||||||
|
description = "IGNOU Telegram Bot v2.0";
|
||||||
|
|
||||||
|
inputs.nixpkgs.url = "github:NixOs/nixpkgs/nixos-22.11";
|
||||||
|
inputs.flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
|
||||||
|
outputs = inputs @{ self, nixpkgs, flake-utils }: let
|
||||||
|
forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
|
||||||
|
nixpkgsFor = forAllSystems (system: import nixpkgs { inherit system; config.allowUnfree = true; });
|
||||||
|
systems = [ "x86_64-linux" "aarch64-linux" ];
|
||||||
|
|
||||||
|
pythonEnv = pkgs : pkgs.python3.withPackages (p: with p; [
|
||||||
|
pyrogram
|
||||||
|
tgcrypto
|
||||||
|
httpx
|
||||||
|
prettytable
|
||||||
|
beautifulsoup4
|
||||||
|
]);
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
packages = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
ignou = pkgs.stdenv.mkDerivation {
|
||||||
|
pname = "ignou-telegram-bot";
|
||||||
|
version = "23.2.18";
|
||||||
|
src = ./.;
|
||||||
|
installPhase = ''
|
||||||
|
mkdir -p $out/bot
|
||||||
|
cp -r bot/* $out/bot/
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
exec-ignou = pkgs.writeShellScriptBin "start-bot" ''
|
||||||
|
cd ${ignou}
|
||||||
|
${pythonEnv pkgs}/bin/python3 -m bot'';
|
||||||
|
|
||||||
|
in
|
||||||
|
{
|
||||||
|
default = pkgs.buildEnv {
|
||||||
|
name = "ignou-telegram-bot-env";
|
||||||
|
paths = [
|
||||||
|
ignou
|
||||||
|
exec-ignou
|
||||||
|
];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
devShell = forAllSystems (system:
|
||||||
|
let
|
||||||
|
pkgs = nixpkgsFor.${system};
|
||||||
|
in
|
||||||
|
pkgs.mkShell {
|
||||||
|
buildInputs = [
|
||||||
|
(pythonEnv pkgs)
|
||||||
|
];
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
nixosModules.default = import ./nix/module.nix inputs;
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
50
nix/module.nix
Normal file
50
nix/module.nix
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
inputs: {
|
||||||
|
config,
|
||||||
|
lib,
|
||||||
|
pkgs,
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
let
|
||||||
|
cfg = config.services.ignou;
|
||||||
|
ignou = inputs.self.packages.${pkgs.stdenv.hostPlatform.system}.default;
|
||||||
|
|
||||||
|
in
|
||||||
|
with lib;
|
||||||
|
{
|
||||||
|
options = {
|
||||||
|
|
||||||
|
services.ignou = {
|
||||||
|
|
||||||
|
enable = mkEnableOption "Enable the IGNOU Bot service";
|
||||||
|
|
||||||
|
BOT_TOKEN = mkOption {
|
||||||
|
type = types.str;
|
||||||
|
description = "Telegram Bot Token";
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
config = mkIf cfg.enable {
|
||||||
|
|
||||||
|
systemd.services.ignou = {
|
||||||
|
|
||||||
|
description = "IGNOU Telegram Bot";
|
||||||
|
|
||||||
|
after = [ "network.target" ];
|
||||||
|
|
||||||
|
wantedBy = [ "multi-user.target" ];
|
||||||
|
|
||||||
|
serviceConfig = {
|
||||||
|
Type = "oneshot";
|
||||||
|
ExecStart = "${ignou}/bin/start-bot";
|
||||||
|
};
|
||||||
|
|
||||||
|
environment = {
|
||||||
|
BOT_TOKEN = cfg.BOT_TOKEN;
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in a new issue