diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0389f22 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +* +!.gitignore +!*/ +!*.java +!lib/* +!config/config.example.yaml diff --git a/config/config.example.yaml b/config/config.example.yaml new file mode 100644 index 0000000..f798abb --- /dev/null +++ b/config/config.example.yaml @@ -0,0 +1,23 @@ +# 每轮查询的间隔(秒),建议设置为600以上 +interval: 600 +# 记录当前成绩的文件(JSON) +record-file: grades-record.json + +# 需要查询的学生 +students: + - id: 学生1学号 + password: 学生1密码 + email: 学生1邮箱 + - id: 学生2学号 + password: 学生2密码 + email: 学生2邮箱 + # 更多的学生用类似格式添加 + +# 发件人邮箱设置(默认使用SSL/TLS) +email: + address: yuki@yuki-nagato.com # 邮箱地址 + sender-name: 長門有希 # 发件人显示姓名 + username: username # SMTP登录用户名 + password: password # SMTP登录密码 + server: smtp.example.com # SMTP服务器地址 + port: 465 # SMTP服务器端口 diff --git a/lib/javax.mail.jar b/lib/javax.mail.jar new file mode 100644 index 0000000..dd06a6a Binary files /dev/null and b/lib/javax.mail.jar differ diff --git a/lib/json-20171018.jar b/lib/json-20171018.jar new file mode 100644 index 0000000..cad0658 Binary files /dev/null and b/lib/json-20171018.jar differ diff --git a/lib/yamlbeans-1.13.jar b/lib/yamlbeans-1.13.jar new file mode 100644 index 0000000..0302ae6 Binary files /dev/null and b/lib/yamlbeans-1.13.jar differ diff --git a/src/com/yuki_nagato/csunotifier/Config.java b/src/com/yuki_nagato/csunotifier/Config.java new file mode 100644 index 0000000..3103a28 --- /dev/null +++ b/src/com/yuki_nagato/csunotifier/Config.java @@ -0,0 +1,38 @@ +package com.yuki_nagato.csunotifier; + +import com.esotericsoftware.yamlbeans.YamlException; +import com.esotericsoftware.yamlbeans.YamlReader; + +import java.io.*; +import java.util.List; +import java.util.Map; + +public class Config { + public final int interval; + public final String recordFile; + public final List> students; + public final Map email; + + public Config(File yamlFile) throws IOException { + YamlReader reader; + try { + reader = new YamlReader(new InputStreamReader(new FileInputStream(yamlFile),"utf-8")); + } + catch (FileNotFoundException e) { + System.err.println("配置文件 "+yamlFile.getPath()+" 未找到"); + throw e; + } + try { + Map map = (Map)reader.read(); + reader.close(); + interval = Integer.parseInt((String)map.get("interval")); + recordFile = (String)map.get("record-file"); + students = (List)map.get("students"); + email = (Map)map.get("email"); + } + catch (YamlException | ClassCastException e) { + System.err.println("配置文件 "+yamlFile.getPath()+" 格式错误"); + throw e; + } + } +} diff --git a/src/com/yuki_nagato/csunotifier/Grade.java b/src/com/yuki_nagato/csunotifier/Grade.java new file mode 100644 index 0000000..20e10d1 --- /dev/null +++ b/src/com/yuki_nagato/csunotifier/Grade.java @@ -0,0 +1,57 @@ +package com.yuki_nagato.csunotifier; + +import org.json.JSONObject; + +import java.util.Map; + +public class Grade { + public final String course,orientationTerm,getTerm,processScore,examScore,mark,credit; + public Grade(String course,String orientationTerm, String getTerm, String processScore, String examScore, String mark, String credit) { + this.course = course; + this.orientationTerm = orientationTerm; + this.getTerm = getTerm; + this.processScore = processScore; + this.examScore = examScore; + this.mark = mark; + this.credit = credit; + } + public Grade(Map map) { + course = map.get("course"); + orientationTerm = map.get("orientationTerm"); + getTerm = map.get("getTerm"); + processScore = map.get("processScore"); + examScore = map.get("examScore"); + mark = map.get("mark"); + credit = map.get("credit"); + } + + public JSONObject toJson() { + JSONObject rst = new JSONObject(); + rst.put("course", course); + rst.put("orientationTerm", orientationTerm); + rst.put("getTerm", getTerm); + rst.put("processScore", processScore); + rst.put("examScore", examScore); + rst.put("mark", mark); + rst.put("credit", credit); + return rst; + } + + @Override + public boolean equals(Object obj) { + if(obj.getClass()!=Grade.class) return false; + Grade b = (Grade)obj; + return course.equals(b.course) && + orientationTerm.equals(b.orientationTerm) && + getTerm.equals(b.getTerm) && + processScore.equals(b.processScore) && + examScore.equals(b.examScore) && + mark.equals(b.mark) && + credit.equals(b.credit); + } + + @Override + public int hashCode() { + return course.hashCode(); + } +} diff --git a/src/com/yuki_nagato/csunotifier/MailSender.java b/src/com/yuki_nagato/csunotifier/MailSender.java new file mode 100644 index 0000000..bcf1abf --- /dev/null +++ b/src/com/yuki_nagato/csunotifier/MailSender.java @@ -0,0 +1,65 @@ +package com.yuki_nagato.csunotifier; + +import javax.mail.*; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.io.UnsupportedEncodingException; +import java.util.Properties; + +public class MailSender { + private final InternetAddress from; + private final String server, username, password, port, senderName; + + public MailSender(String address, String name, String server, String username, String password, String port) throws UnsupportedEncodingException { + this.from = new InternetAddress(address, name, "utf-8"); + this.server = server; + this.username = username; + this.password = password; + this.port = port; + this.senderName = name; + } + public void send(Student student, Diff diff) throws MessagingException, UnsupportedEncodingException { + Properties props = new Properties(); + props.put("mail.smtp.host", server); + props.put("mail.smtp.port", port); + props.put("mail.smtp.ssl.enable", "true"); + props.put("mail.smtp.auth", "true"); + Session session = Session.getInstance(props, new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + MimeMessage message = new MimeMessage(session); + message.setFrom(from); + message.setRecipient(Message.RecipientType.TO, new InternetAddress(student.email,student.getName(),"utf-8")); + message.setSubject("中南大学新成绩通知","utf-8"); + message.setContent(generateContent(student, diff), "text/html;charset=utf-8"); + message.saveChanges(); + Transport.send(message); + } + + String generateContent(Student student, Diff diff) { + StringBuilder rst = new StringBuilder(); + rst.append("

").append(student.getName()).append("同学:

"); + rst.append("

您有成绩变化:

"); + if(!diff.additions.isEmpty()) { + rst.append("

新增的成绩为

"); + rst.append(""); + for(Grade grade : diff.additions) { + rst.append(String.format("", grade.course, grade.orientationTerm, grade.getTerm, grade.processScore, grade.examScore, grade.mark, grade.credit)); + } + rst.append("
课程初修学期获得学期过程成绩期末成绩成绩学分
%s%s%s%s%s%s%s
"); + } + if(!diff.deletions.isEmpty()) { + rst.append("

消失的成绩为

"); + rst.append(""); + for(Grade grade : diff.deletions) { + rst.append(String.format("", grade.course, grade.orientationTerm, grade.getTerm, grade.processScore, grade.examScore, grade.mark, grade.credit)); + } + rst.append("
课程初修学期获得学期过程成绩期末成绩成绩学分
%s%s%s%s%s%s%s
"); + } + rst.append("

Yours sincerely,
").append(senderName).append("

"); + return rst.toString(); + } +} diff --git a/src/com/yuki_nagato/csunotifier/Main.java b/src/com/yuki_nagato/csunotifier/Main.java new file mode 100644 index 0000000..a89367a --- /dev/null +++ b/src/com/yuki_nagato/csunotifier/Main.java @@ -0,0 +1,45 @@ +package com.yuki_nagato.csunotifier; + +import javax.mail.MessagingException; +import java.io.File; +import java.io.IOException; +import java.util.Map; + +public class Main { + public static void main(String[] args) throws IOException, InterruptedException { + final Config cfg = new Config(new File("config/config.yaml")); + Student.recordFile = new File(cfg.recordFile); + Student.load(); + MailSender sender = new MailSender(cfg.email.get("address"),cfg.email.get("sender-name"),cfg.email.get("server"),cfg.email.get("username"),cfg.email.get("password"),cfg.email.get("port")); + while(true) { + for(Map student : cfg.students) { + String id = student.get("id"), password = student.get("password"), email = student.get("email"); + try { + System.out.println("开始查询"+id); + Student stu = new Student(id,password,email); + Diff diff = stu.check(); + if(diff.additions.isEmpty() && diff.deletions.isEmpty()) { + System.out.println("没有变化"); + } + else { + System.out.printf("新增%d个成绩,减少%d个成绩\n", diff.additions.size(), diff.deletions.size()); + sender.send(stu,diff); + System.out.println("邮件发送成功"); + } + } + catch (MessagingException e) { + System.err.println("邮件发送失败"); + e.printStackTrace(); + } + catch (AuthenticationException e) { + System.err.println("用户名或密码错误"); + } + catch (NullPointerException | IOException e) { + System.err.println("关于"+id+"的配置可能有误"); + e.printStackTrace(); + } + Thread.sleep(cfg.interval*1000/cfg.students.size()); + } + } + } +} diff --git a/src/com/yuki_nagato/csunotifier/ScorePage.java b/src/com/yuki_nagato/csunotifier/ScorePage.java new file mode 100644 index 0000000..daaeea6 --- /dev/null +++ b/src/com/yuki_nagato/csunotifier/ScorePage.java @@ -0,0 +1,80 @@ +package com.yuki_nagato.csunotifier; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.*; +import java.util.ArrayList; +import java.util.Base64; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class ScorePage { + private final String content; + + public ScorePage(String username, String password) throws IOException, AuthenticationException { + URL url = new URL("http://csujwc.its.csu.edu.cn/jsxsd/xk/LoginToXk"); + HttpURLConnection connection = (HttpURLConnection)url.openConnection(); + connection.setRequestMethod("POST"); + connection.setInstanceFollowRedirects(false); + connection.setDoOutput(true); + connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + String encoded = Base64.getEncoder().encodeToString(username.getBytes("utf-8")) + + "%%%" + + Base64.getEncoder().encodeToString(password.getBytes("utf-8")); + connection.getOutputStream().write(("encoded="+ URLEncoder.encode(encoded,"utf-8")).getBytes("utf-8")); + if(connection.getResponseCode()!=302) { + throw new AuthenticationException(); + } + List cookies = connection.getHeaderFields().get("Set-Cookie"); + + // get page + url = new URL("http://csujwc.its.csu.edu.cn/jsxsd/kscj/yscjcx_list"); + connection = (HttpURLConnection)url.openConnection(); + for(String cookie : cookies) { + connection.addRequestProperty("Cookie", cookie.substring(0,cookie.indexOf(';'))); + } + BufferedReader br = new BufferedReader(new InputStreamReader(connection.getInputStream(), "utf-8")); + StringBuilder body = new StringBuilder(); + String line; + while((line = br.readLine())!=null) { + body.append(line); + body.append('\n'); + } + + //exit + url = new URL("http://csujwc.its.csu.edu.cn/jsxsd/xk/LoginToXk?method=exit"); + connection = (HttpURLConnection)url.openConnection(); + connection.setInstanceFollowRedirects(false); + for(String cookie : cookies) { + connection.addRequestProperty("Cookie", cookie.substring(0, cookie.indexOf(';'))); + } + connection.getResponseCode(); + + content = body.toString(); + } + public ArrayList getGrades() { + String pt = ".*?.*?(.*?).*?(.*?).*?(.*?).*?(.*?).*?(.*?).*?,700,500\\)\">(.*?).*?(.*?)"; + Pattern pattern = Pattern.compile(pt, Pattern.DOTALL); + Matcher m = pattern.matcher(content); + ArrayList rst = new ArrayList<>(); + while(m.find()) { + rst.add(new Grade(m.group(3),m.group(1),m.group(2),m.group(4),m.group(5),m.group(6),m.group(7))); + } + return rst; + } + public String getName() { + String pt = "  (.*)\\("; + Pattern pattern = Pattern.compile(pt); + Matcher m = pattern.matcher(content); + m.find(); + return m.group(1); + } +} + +class AuthenticationException extends Exception { + AuthenticationException() { + super("用户名或密码错误"); + } +} \ No newline at end of file diff --git a/src/com/yuki_nagato/csunotifier/Student.java b/src/com/yuki_nagato/csunotifier/Student.java new file mode 100644 index 0000000..ff71ceb --- /dev/null +++ b/src/com/yuki_nagato/csunotifier/Student.java @@ -0,0 +1,106 @@ +package com.yuki_nagato.csunotifier; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.*; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class Student { + static File recordFile; + static HashMap> lasts; + final String id, password, email; + private final ScorePage sp; + public Student(String id, String password, String email) throws IOException, AuthenticationException { + this.id = id; + this.password = password; + this.email = email; + sp = new ScorePage(id, password); + } + private ArrayList getLastGrades() { + return lasts.get(id); + } + private ArrayList getNewGrades() { + return sp.getGrades(); + } + public String getName() { + return sp.getName(); + } + private void save(ArrayList grades) { + lasts.put(id, grades); + JSONObject out = new JSONObject(); + for(Map.Entry> student : lasts.entrySet()) { + JSONArray gradesArr = new JSONArray(); + for(Grade grade : student.getValue()) { + gradesArr.put(grade.toJson()); + } + out.put(student.getKey(), gradesArr); + } + try { + // fucking encoding + //FileWriter fw = new FileWriter(recordFile); + OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(recordFile),"utf-8"); + fw.write(out.toString()); + fw.close(); + } + catch (IOException e) { + System.err.println("写入文件 "+recordFile.getPath()+" 失败"); + e.printStackTrace(); + } + } + public Diff check() { + ArrayList last = getLastGrades(), now = getNewGrades(); + if(last==null) { + save(now); + return new Diff(new ArrayList<>(), new ArrayList<>()); + } + Diff rst = new Diff(); + rst.additions = (ArrayList) now.clone(); + rst.additions.removeAll(last); + rst.deletions = (ArrayList) last.clone(); + rst.deletions.removeAll(now); + if(!rst.additions.isEmpty() || !rst.deletions.isEmpty()) { + save(now); + } + return rst; + } + public static void load() throws IOException { + try { + BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(recordFile),"utf-8")); + StringBuilder sb = new StringBuilder(); + String line; + while((line = br.readLine())!=null) { + sb.append(line); + } + Map allTheStudents = new JSONObject(sb.toString()).toMap(); + lasts = new HashMap<>(); + for(Map.Entry student : allTheStudents.entrySet()) { + ArrayList grades = new ArrayList<>(); + for(Object grade : (List)(student.getValue())) { + grades.add(new Grade((Map) grade)); + } + lasts.put(student.getKey(), grades); + } + } + catch (FileNotFoundException e) { + //FileWriter fw = new FileWriter(recordFile); + OutputStreamWriter fw = new OutputStreamWriter(new FileOutputStream(recordFile),"utf-8"); + fw.write(new JSONObject().toString()); + fw.close(); + lasts = new HashMap<>(); + } + } +} + +class Diff { + ArrayList additions; + ArrayList deletions; + Diff(){} + Diff(ArrayList additions, ArrayList deletions) { + this.additions=additions; + this.deletions=deletions; + } +}