IGNOU Telegram Bot v2.0

This commit is contained in:
roshan 2023-02-19 19:35:17 +05:30
commit 9668553828
7 changed files with 500 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
__pycache__
*.session

65
bot/__main__.py Normal file
View 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
View 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
View 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
View 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
View 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
View 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;
};
};
};
}