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