来对腾讯课堂操作一波

COVID-19 病毒大范围流行,从二月到四月,从假期到学校开学,目前各地学校大多都以网络直播的形式授课,不少学校(例如我们学校)选择了腾讯课堂。让我来搞一波 Web 版的腾讯课堂 #(滑稽),整上签到/答题通知,防止在划水的时候翻船(逃。另外还有实时画面截取、快速清人、去掉水印、OBS 录屏等内容的操作分享。

只看成品可直接点下面:

使用网页版腾讯课堂的优点:

  1. 易搞
  2. 权限低(确保不被监控屏幕,防止系统级调用)
  3. 还是易搞

P.S. 腾讯课堂在最新版的 Chrome 上似乎会遇到一些奇奇怪怪的问题,换国产浏览器后就好一些了… 上课摸鱼两不误,换一个浏览器专门上课+录屏,可以的!

好的,那我们开搞…

开搞前

申明:渣渣技术,纯属水文儿,若有什么可(cuo)笑(wu)之处,敬请嘲(zhi)笑(chu)

先进入一个网课直播页面,直播开始播放

这里从表面可以分析页面 URL:

1
https://ke.qq.com/webcourse/index.html#cid=744445&term_id=10042&taid=5128062000000000&lite=1

得到这些参数:

  • term_id 直播的房间号,不同的老师直播有不同的 term_id,这是关键的参数
  • cid 暂不知道具体是啥,就是一个 ID 吧
  • taid 他ID,猜测是腾讯课堂账号的 ID
  • lite 不明

然后打开 DevTools

流量分析

打开 Network 选项卡,对网络流量进行分析

report_vm

首先看到的是对 https://cgi.connect.qq.com/report/report_vm 的 GET 请求,有三个参数:

  • monitors 它的值是一个 Array 类型中包含多个 Number,结合键和值来看,似乎是汇报当前的状态(网络状态之类的),这些 Number 是不同的状态码

  • t 可能是和时间相关的值,结合多个 report_vm 请求来看,这个值是在变动的

  • _ 第一次请求值是 ,,, 第二次值是 , 暂不明是什么东西

结论:JS 会生成 monitors 的值(包含用户当前的一些网络信息等),对 report_vm 进行不间断请求,上报用户当前网络状态等

base_pull

第二个看到的是对 https://pull.ke.qq.com/base_pull 的 POST 请求,同时也携带 URL 参数

URL Query 为 bkn=60382&t=0.3868,这个 bkn 参数每次请求的值都是固定的,就一串 Number;t 这个参数一看就是和时间相关的(可以防止浏览器缓存)

这里的 Form Data 是一个 JSON 对象(看参数的名字,让我来猜猜是什么东西):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"biz_id": "edu", // 类型相关
"room_id": 10042, // 房间号,这和当前页面 URL 参数 `term_id` 一样,∴ room_id≈term_id
"platform": 3, // 平台类型的 ID号,腾讯课堂有几种平台 Web, App, iOS, iPad, Android
"version": 0, // 版本号
"room_seq_str": "0", // tcp 中 seq 是 Sequence 的缩写
"uid_str": "10000", // User ID,这就是你的 QQ 号
"last_time_interval": 10, // interval,和 JS 定时器相关的参数?

// 懒得猜了... 对我来说似乎也并没有什么x用
"room_reliable_req": {
"room_reliable_seq": 167,
"req_type": 1
},
"user_reliable_req": {
"user_reliable_seq": 85,
"req_type": 1
}
}

结论:告知服务器此时用户正在上什么课,用什么平台上,等等

report

当你点击一下任意位置,就会有一个对 https://tmapp.qq.com/cgi-bin/activity_platform/tdw/report 的 GET 请求

请求参数主要是 fieldsdatas,前者是数据的键,后者包含数据的值

对参数 datas 稍做一些处理

结论:通过观察,答题/签到/点击鼠标,都会走这个地址~ 你的一举一动就是从这里被观察到的哦

你的指针在那个页面点了任何位置,都会报告给服务器

浏览器页面失去焦距(干其他的事情去了),目前不会发送消息给服务器报告

web_report

https://report.edu.qq.com/data_report/web_report POST 请求

向服务器汇报操作结果等内容

gettoken

https://ke.qq.com/cgi-bin/web_ke/gettoken GET 请求,获取 token

more…

此外,还有一大堆 http 请求,也有 websocket,真 tm 复杂….

代码欣赏

好,接下来… 让我们来欣赏一下腾讯课堂的代码吧~

首先… 来到 Console,一堆 Cross-Origin 的 Warning

哪位大哥是把 hook 错误拼写成了 hack 吗?

这个 index.html 插了一堆 <script> 里面直接写的 JS 代码,极其凌乱,有赶工的嫌疑呢 #(滑稽)

还有那个百度统计有点东西… 怪不得看到一堆百度 favicon.ico 的请求

转了一圈,确定腾讯课堂的 Ui 采用了 React 框架

这个 report_vm 请求的数据就是当前网络状态的一些状态码

(P.S. 此处有被未格式化代码淹没的我)

弄一弄,找到了部分状态码的含义

Chrome 里面太卡,可以把 index.js 搞来本地格式化一下

emmm… 想要彻底搞清楚这些状态码具体是什么意思,对我来说好像不太现实… 状态码太多了…

这里还找到了一个 window 中的对象,可获取用户个人信息

断点观察,方法函数调用进进出出,上上下下,反反复复……

114612 行代码,在 Chrome 里面打开真的有点吃不消

过程懒得写了… 总之整出了一些有参考价值的代码 ↓

https://gist.github.com/qwqcode/051f8398ba12bdc325a6709a27a50fd8

Report

这就是之前看到发出 tmapp.qq.com/cgi-bin/activity_platform/tdw/report 请求相关的代码

答题卡/签到 Modal

由于老师迟迟不来一波签到,不太容易抓到这个元素,从而得到可以判断当前是否处于答题/签到中的 selector

所以,只好看代码了… 主要是看 Render 相关的代码,就可以得到 Selector

由代码可知,当开始签到/答题时,会生成一个新组件(组件名为:ImDialog),classname 分别为 sign-dialogexamcard-dialog

例如写一段 JS 通过一直查询 document 中的 .examcard-dialog 元素是否存在,就可以判断当前是否正在答题

ImDialog

腾讯课堂中经常复用的 Dialog 组件,当弹出窗口时,会有一个新的 .im-dialog 元素出现

划水防止翻车通知

腾讯课堂升级了一下,老师导出上课成员名单表格,可以看到在上课期间未点打卡的人

新版的腾讯课堂老师端,可以看到当前答题的详细情况,哪些人没有答题…

如果老师更新了,就不敢在答题的时候摸鱼了(哼… 某些老师课都不备,只知道上课喊做题… 自己都要选错答案,乱qiu讲

新版腾讯课堂可查看答题明细

实时检测是否存在 答题/签到 Dialog 元素,当元素存在时 执行通知操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
SEL_EXAM = '.examcard-dialog .dialog-body';
SEL_SIGN = '.sign-dialog .dialog-body';

ltExamEl = null; // 最新的答题卡元素
ltSignEl = null; // 最新的签到元素

setInterval(() => {
// 弹出答题窗口
const examEl = document.querySelector(SEL_EXAM);
if (examEl !== null && examEl !== ltExamEl) {
ltExamEl = examEl;

console.log("检测到 答题卡,开始执行发送 QQ 消息");
}

// 弹出签到窗口
const signEl = document.querySelector(SEL_SIGN);
if (signEl !== null && signEl !== ltSignEl) {
ltSignEl = signEl;

console.log("检测到 签到,开始执行发送 QQ 消息");
}

}, 1000); // 1s检测一次

我使用了 酷Q + CQHttp 插件,在网页上和 酷Q 建立通讯,从而发送 QQ消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 发送私信
QWQ.SendQQMsg = (qqNum, msg, name) => {
if (!msg) return;
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200)
console.log(`[酷Q] 发私信 NAME:${name} QQ:${qqNum}`, JSON.parse(xhr.response));
};
xhr.open("get", `http://127.0.0.1:12307/send_private_msg_rate_limited?user_id=${encodeURIComponent(qqNum)}&message=${encodeURIComponent(msg)}`, true);
xhr.send(null);
};

// 发送群消息
QWQ.SendGrpMsg = (grpNum, msg) => {
if (!msg) return;
const xhr = new XMLHttpRequest();
xhr.onreadystatechange = () => {
if (xhr.readyState === 4 && xhr.status === 200)
console.log(`[酷Q] 发群消息 GRP:${grpNum}`, JSON.parse(xhr.response));
};
xhr.open("get", `http://127.0.0.1:12307/send_group_msg_rate_limited?group_id=${encodeURIComponent(grpNum)}&message=${encodeURIComponent(msg)}`, true);
xhr.send(null);
};

JS 实时画面截取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 获取当前直播画面,并创建 canvas 对象来返回
*/
function GetVideoShotCanvas (width, height, filter) {
const video = document.querySelector("#main_video");
if (video === null) return null;
const canvas = document.createElement("canvas");
canvas.width = width || 1280;
canvas.height = height || 720;
const ctx = canvas.getContext("2d");
if (filter) {
ctx.filter = filter;
}
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
return canvas;
}

/**
* 获取当前直播画面,并返回 Base64 DataURL
*/
function GetVideoShotDataURL (quality, width, height) {
if (typeof quality === "undefined") quality = 0.6;
const canvas = QWQ.GetVideoShotCanvas(width, height);
if (canvas === null) return null;
return canvas.toDataURL("image/jpeg", quality); // Base64
}

可以做一个自动截图功能,对于 PPT 讲课比较友好

我有一个思路:首先截取直播的一帧,间隔几秒,再截取一帧。通过对比前后两帧的相似度,相似度过小则保存前一帧即可

JS 相似度对比可以使用现成的算法,这里找到了一个:

https://github.com/obartra/ssim

快速清人

快速找出未到的成员

复杂的事情为什么不交给可耐的 JS 来做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
(function (queryList) {
const elemList = document.querySelectorAll('.member-list .member-item-inner');
const nameList = [];
elemList.forEach((item) => { nameList.push(item.innerText); })

const inHereList = [];
const notHereList = [];
queryList.forEach((name) => {
let isExits = false;
nameList.forEach((n) => {
if (n.indexOf(name) !== -1) isExits = true;
})
if (isExits) inHereList.push(name); else notHereList.push(name);
});
console.log(`============ ${new Date} ============`);
console.log("[列表获取的成员]", nameList);
console.log("[计算-存在的成员]", inHereList);
console.log("[计算-不存在的成员]", notHereList);
console.log(`\n\n`);
})(["小A", "小B", "小C", ...]); // 班上完整成员名单

复制粘贴,控制台敲入即可

P.S. 这里自动翻页没做,让我摸摸鱼吧… 你自己手动点翻页 qwq

去掉正在观看悬浮水印

上网课的时候,你的QQ号有时会飘来飘去,烦得要死

录屏截图的时候也会泄露你的 QQ

复制下面的代码到暴力猴里就可以去掉

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==UserScript==
// @icon http://9.url.cn/edu/lego_modules/edu-ui/0.0.1/img/nohash/logo_pc_rich.png
// @name 腾讯课堂 去除悬浮水印
// @namespace http://tampermonkey.net/
// @version 0.1
// @author qwqcode
// @match *://ke.qq.com/webcourse/index.html*
// @run-at document-end
// ==/UserScript==

(function() {
const style = document.createElement('style');
style.innerHTML = `#marquee, .player-inject { opacity: 0 !important;position: absolute !important;top: -100000px !important; }`;
document.head.appendChild(style);
})();

至于怎么安装暴力猴插件,百度 “安装暴力猴”

去掉其它多余元素(for 录屏)

暴力猴代码,去掉悬浮水印、答题/签到弹窗置于右上、评论区域改造、移除可能暴露个人信息的元素:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// ==UserScript==
// @icon http://9.url.cn/edu/lego_modules/edu-ui/0.0.1/img/nohash/logo_pc_rich.png
// @name 腾讯课堂【录制专用 CSS】清除水印 去除多余元素
// @namespace http://tampermonkey.net/
// @version 0.1
// @author qwqcode
// @match *://ke.qq.com/webcourse/index.html*
// @run-at document-end
// ==/UserScript==

(function() {
const style = document.createElement('style');
style.innerHTML = `.examcard-dialog .im-dialog{transform:translate(664px,-483px) !important;width:295px !important;min-width:295px !important;}.video-cont .main-video{background:#434343;}.im-dialog-mask{background-color:transparent;}.ke_overlay_content .download-tips{opacity:0;}#react-body .qrcode-maker{opacity:0;}.study-header .operations .header-item.real-name,.s-avatar .avatar-img{opacity:0 !important;}#marquee, .player-inject{opacity:0 !important;position:absolute !important;top:-100000px !important;}#react-body.web .chat-ctn .chat-wrap .member-item{height:26px !important;min-height:26px !important;padding:0px 0 0px 10px !important;font-size:16px;border-left:3px solid #188eee;margin-left:-3px;}.member-item-content{padding-top:0px !important;}#react-body.web .chat-ctn .chat-item-msg{margin-top:-4px !important;background:#000 !important;font-size:16px;margin:10px;border-radius:4px;width:unset;padding:6px 0 0 0;}.member-item-nickname{height:26px !important;}.chat-item-msg-content > span{color:#FFF !important;font-family:"Microsoft YaHei Light" !important;}img#img_1149527164{opacity:0;}span.uin{opacity:0;}span#nick_1149527164{opacity:0;}span#qr_area{opacity:0;}div#qlogin_tips_1{opacity:0;}div#title_0{opacity:0;}div#bottom_qlogin{opacity:0;}.video-cont .main-video{height:100%;}.webrtc-ctn .video-cont .video-controls#videoControls{opacity:.75;background:transparent;}.study-header .operations .share.header-item{display:none;}.study-header .operations .mobile-watch.header-item{display:none;}.study-header .operations .header-item.etc{display:none;}.video-cont .video-controls .control-item.time{border-right:0 !important;color:#000;}.video-cont .video-controls .right-panel .control-item{}.video-cont .video-controls .control-item .icon{color:#000;}.vcp-panel-bg{background-color:transparent;}`;
document.head.appendChild(style);
})();

原始界面:

修改后:

敏感信息都没有了

OBS 录屏添加评论区

这是 OBS 录屏添加右下评论区方法

创建一个新的“显示器获捕”

右键“滤镜”,添加“色值”

  • 关键颜色类型:自定义颜色
  • 关键颜色:#333333
  • 相似度:1
  • 平滑:1
  • 不透明度:42 (整来半透明)
  • 对比度:1.00
  • 亮度:-1.00
  • 伽马:0.16

右键“变换”,“编辑变换”

“裁切”参数改成适合的值

- END -

本站文章除注明外均采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 协议进行许可 ヾ(゚ー゚ヾ)
分享到