From 96685538288bcffd886f6cca216a7b3273adc76d Mon Sep 17 00:00:00 2001 From: roshan Date: Sun, 19 Feb 2023 19:35:17 +0530 Subject: [PATCH] IGNOU Telegram Bot v2.0 --- .gitignore | 2 + bot/__main__.py | 65 ++++++++++++++ bot/ignou.py | 224 ++++++++++++++++++++++++++++++++++++++++++++++++ flake.lock | 44 ++++++++++ flake.nix | 50 +++++++++++ flake.nix.bak | 65 ++++++++++++++ nix/module.nix | 50 +++++++++++ 7 files changed, 500 insertions(+) create mode 100644 .gitignore create mode 100644 bot/__main__.py create mode 100644 bot/ignou.py create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 flake.nix.bak create mode 100644 nix/module.nix diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..28a7b2a --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__ +*.session \ No newline at end of file diff --git a/bot/__main__.py b/bot/__main__.py new file mode 100644 index 0000000..fe6a67b --- /dev/null +++ b/bot/__main__.py @@ -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"Result checked by {message.from_user.mention(message.from_user.first_name)}" + 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"
{result['table']}
\n{footer}", + parse_mode=ParseMode.HTML, + ) + elif message.chat.type == ChatType.PRIVATE: + await message.reply_text( + f"
{result['header']}{result['table']}
", + parse_mode=ParseMode.HTML, + ) + + +if __name__ == "__main__": + app.run() diff --git a/bot/ignou.py b/bot/ignou.py new file mode 100644 index 0000000..0422640 --- /dev/null +++ b/bot/ignou.py @@ -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()} diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..1e9a0ab --- /dev/null +++ b/flake.lock @@ -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 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..946c238 --- /dev/null +++ b/flake.nix @@ -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; + + }); +} diff --git a/flake.nix.bak b/flake.nix.bak new file mode 100644 index 0000000..5616644 --- /dev/null +++ b/flake.nix.bak @@ -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; + + }; +} diff --git a/nix/module.nix b/nix/module.nix new file mode 100644 index 0000000..d06fabc --- /dev/null +++ b/nix/module.nix @@ -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; + }; + + }; + + }; +} \ No newline at end of file