def __init__(self, user): # Touch files open(cookieFilename, "a").close() open(courseFilename, "a").close() # Init data self.loggedIn = False self.asciiout = ASCIIOutput(80) self.user = user self.courses = {} self.cache = self.loadCache() self.cookies = self.initCookies()
class StudICLI: """ StudICLI, a simple command line client class for Stud.IP as in use at the university of Passau """ def __init__(self, user): # Touch files open(cookieFilename, "a").close() open(courseFilename, "a").close() # Init data self.loggedIn = False self.asciiout = ASCIIOutput(80) self.user = user self.courses = {} self.cache = self.loadCache() self.cookies = self.initCookies() def initCookies(self): """ Stud.IP requires cookies for session-handling. Cookies are stored in a file to limit login requests""" cookieFile = open(cookieFilename, "r+") cookies = LWPCookieJar(cookieFile.name) if len(cookieFile.read()) > 0: cookies.load(cookieFile.name, ignore_discard=True) return cookies def loadCache(self): # Courses are temporarily stored to limit requests courseFile = open(courseFilename, "rb+") if len(courseFile.read()) > 0: courseFile.seek(0) data = pickle.load(courseFile) # The cache is per-user if self.user != data["user"]: # Invalidate cached cookies open(cookieFilename, "w").close() data["courses"] = {} else: self.courses = data["courses"] return data return {"courses":{}, "user":""} def loadCourses(self): # Dictionary of courses. courses = {} # Rely on the cache if it contains entries, read courses from page otherwise storedCourses = self.cache["courses"] if type(storedCourses) == types.DictType and len(storedCourses) > 0: courses = storedCourses if len(courses) == 0: coursePage = pq(self.read("meine_seminare.php")) courseLinks = coursePage("td.blank").siblings("td a[href^=seminar]") def addCourse(id): """ inner function to process courses """ courseName = courseLinks.eq(id).children("font").eq(0).text() if courseName != None: courseId = courseLinks.eq(id).attr("href").replace("seminar_main.php?auswahl=", "") courses[courseId] = courseName.strip() courseLinks.each(addCourse) # Storing the username with the courses allows for invalidation (see loadCache) data = {"courses": courses, "user": self.user} courseFile = open(courseFilename, "rb+") pickle.dump(data, courseFile) courseFile.close() self.courses = courses return courses def req(self, url, body="", headers={}): """ Requests an URL, sends a body via POST and includess arbitrary header options This function is cookie- and redirect-aware""" request = Request(baseUrl+url, body, headers) opener = build_opener(HTTPRedirectHandler(), HTTPCookieProcessor(self.cookies)) response = opener.open(request) #print request.get_full_url(), request.header_items() #print request.get_data() self.cookies.extract_cookies(response, request) self.cookies.save(ignore_discard=True) return response def read(self, url, body="", headers={}): """ read() is a shortcut to req([..]).read() to save some typing of this often used function """ response = self.req(url, body, headers) return response.read() def login(self, user, passwd, domain): """ Execute an Login to Stud.IP. This requires two requests, because the form includes an unique ID.""" loginPage = pq(self.read("login.php")) # If there is no form element on the login page, we are already logged in loginTicket = loginPage("form input[name=login_ticket]").attr("value") loginArgs = "username="******"&password="******"&userdomain=" +domain+"&login_ticket="+loginTicket loginResult = self.req("index.php", body=loginArgs) loggedIn = True self.loadCourses() return True def isLoggedIn(self): """ Calls the login page to see if the session is alive """ loginPage = pq(self.read("login.php")) return not bool(loginPage("form").eq(0)) def courselist(self, indexed=True, standalone=False): """ List courses and display details on demand """ self.asciiout.h1("Meine Veranstaltungen") """ Prints a list of courses """ for i in range(0, len(self.courses)): key = self.courses.keys()[i] if indexed: print "["+str(i)+"] "+self.courses[key] else: print self.courses[key] # courselist() is also utilized by other actions. the standalone flag prevents conflicts if standalone: courseId = selectId("Veranstaltungsdetails", len(self.courses)) coursePage = pq(self.read("seminar_main.php?auswahl="+self.courses.keys()[courseId])) detailPage= pq(self.read("print_seminar.php")) self.asciiout.h1(detailPage("h1").eq(0).text()) rows = detailPage("table").eq(0).find("tr") for i in range(1, len(rows)): tds = rows.eq(i).children("td") self.asciiout.h2(tds.eq(0).text()) self.asciiout.text(br2nl(tds.eq(1)).text()) def timetable(self): """ Prints a timetable """ self.asciiout.h1("Mein Stundenplan") timetablePage = pq(self.read("mein_stundenplan.php")) coursePlan = timetablePage("#content table table tr td.rahmen_white") days = [[], [], [], [], [], [], []] lineSize = [2] def scheduleCourse(id): """ inner function that adds each course to the timetable. """ course = coursePlan.eq(id) infos = course.children("table td font") timeNroom = infos[0].text_content() event = infos[1].text_content() lineSize.append(len(event)) teacher = infos[2].text_content() day = len(course.prevAll())-1 days[day].append((timeNroom, event, teacher)) coursePlan.each(scheduleCourse) self.asciiout.hr() for day in range(0, len(days)): """ Locale-dependent output of weekday. While it makes little sense with the hardcoded German strings everywhere, we avoid typing the names. """ self.asciiout.h2(day_name[day]) for course in days[day]: print course[1] print course[0] print "gelesen von: "+course[2] self.asciiout.hr() def download(self, new=False, all=False): """ Download files of a course""" courseId = 0 if not all: cli.courselist() courseId = selectId("Veranstaltung", len(self.courses) -1) self.asciiout.h1("Dateien herunterladen") idRange = range(0, len(self.courses)) if all else [courseId] for id in idRange: filePage = pq(self.read("seminar_main.php?auswahl="+self.courses.keys()[id]+"&redirect_to=plugins.php&cmd=show&id=19&view=seminarFolders")) keyword = ("Neue Dateien " if new else "")+"komprimiert herunterladen" downloadLink = filePage('a[title="'+keyword+'"]').eq(0).attr("href") courseName = self.courses[self.courses.keys()[id]] if downloadLink != None: print "Lade Dateien in '"+courseName+"' herunter" data = self.read(downloadLink.replace(baseUrl, "")) if len(data) > 0: fileName = "Downloads/"+safeName(courseName)+".zip" if not os.path.exists("Downloads"): os.makedirs("Downloads") f = open(fileName, "w+") f.write(data) f.close() self.asciiout.text("Dateien in '"+courseName+"' unter "+fileName+" gespeichert") self.asciiout.hr() continue self.asciiout.text("Keine Dateien in '"+courseName+"' gefunden") self.asciiout.hr() def readnews(self, all=False): """ Read news for a Course """ cli.courselist() if not all: courseId = selectId("Veranstaltung", len(cli.courses) -1) width = 70 idRange = range(0, len(self.courses)) if all else [courseId] for id in idRange: self.asciiout.h1("News fuer "+self.courses[self.courses.keys()[id]]+" anzeigen") newsPage = pq(self.read("seminar_main.php?nclose=TRUE&auswahl="+self.courses.keys()[id])) newsItems = newsPage("a[href*=nopen]").children("img").parent() def printNews(i): """ inner function that prints course news """ newsId = fromQueryString(newsItems.eq(i).attr("href"), "nopen") newsPage = pq(self.read("seminar_main.php?nopen="+newsId)) title, rest = newsPage("a[href*=nclose]").parent().siblings().text().split("|", 1) self.asciiout.h2(title) self.asciiout.text(br2nl(newsPage("td.printcontent").eq(1)).text()) self.asciiout.hr() newsItems.each(printNews) def readposts(self, all=False): """ Read posts from a course's messaging boards """ print cli.courselist() courseId = selectId("Veranstaltung", len(cli.courses) -1) self.asciiout.h1("Posts anzeigen") width = 70 idRange = range(0, len(self.courses)) if all else [courseId] for id in idRange: forumPage = pq(self.read("seminar_main.php?auswahl="+self.courses.keys()[id]+"&redirect_to=forum.php&view=reset&sort=age")) postsPage = pq(self.read("forum_export.php")) posts = postsPage("table td") self.asciiout.h1(self.courses[self.courses.keys()[id]]) # Iterate through the Table with steps of 2. i is the headline, i+1 the body of the post for i in [x for x in range(0, len(posts)-2) if x % 2 ==0]: # Forum headlines have h3-tags forum = posts.eq(i).children("h3").text() if forum: self.asciiout.h2(forum) else: head = posts.eq(i).text() self.asciiout.h3(head) br2nl(posts.eq(i+1)) body = posts.eq(i+1).text() self.asciiout.text(body) self.asciiout.hr() def readmessages(self, all=False): """ Read all or one message from the messaging system """ self.asciiout.h1("Nachrichten lesen") messagePage = pq(self.read("sms_box.php?mclose=TRUE")) subjects = messagePage("td.printhead a.tree[href*=mopen]") messages = {} def messageList(id): subject = subjects.eq(id) author, date = subject.parents("td.printhead").eq(0).next().text().split(",", 1) messages[id] = { "hash" : fromQueryString(subject.attr("href"), "mopen"), "subject": subject.text(), "author": author.strip().replace("von ", ""), "date": date.strip()} if not all: print self.asciiout.trim("["+str(id)+"] "+messages[id]["author"]+": "+subject.text()) subjects.each(messageList) if not all: id = selectId("Nachricht", len(messages)-1) messages = {id: messages[id]} for message in messages: messagePage = pq(self.read("sms_box.php?mopen="+messages[message]['hash'])) msg = messages[message] self.asciiout.h2(msg["author"]+": "+msg["subject"]+" ("+msg["date"]+")") self.asciiout.text(br2nl(messagePage("td.printcontent")).text()+"\n") if not all: respond = raw_input("Auf diese Nachricht anworten? [j/N] ") if respond in ["j", "J", "y", "Y"]: self.writemessage(answerTo=msg["hash"]) def writemessage(self, recipient="", answerTo=""): """ Send message to another Stud.IP user """ self.asciiout.h1("Nachricht schicken") if not len(recipient): if len(answerTo) == 0: recipient = raw_input("Empfänger: ") subject = raw_input("Betreff: ") self.read("sms_send.php", body="&add_freesearch.x=5&add_freesearch.y=11&add_freesearch&freesearch[]="+recipient) else: responsePage = pq(self.read("sms_send.php?cmd=write&answer_to="+answerTo)) subject = responsePage("form input[name=messagesubject]").attr("value") body = "" emptyLines = 0 print "Nachricht (Mit zwei Leerzeilen abschließen): " while emptyLines < 2: line = raw_input() body += line+"\n" if len(line): emptyLines = 0 else: emptyLines += 1 body.strip() # Strip the extra lines args = "messagesubject="+subject+"&message="+body+"&cmd_insert.x=1&cmd_insert.y=1" if len(answerTo) > 0: args += "answerTo"+answerTo response = self.read("sms_send.php", body=args) if response.find("wurde verschickt!") != -1: print "Nachricht erfolgreich verschickt" else: print "Nachricht konnte nicht verschickt werden"