【hook】下载 Telegram 受保护的媒体资源

开篇废话

最近一朋友问了这个问题,研究了一下安排了一个看上去比较稳的方案。
由于使用的是官方客户端 + hook 方式,理论上风险比 userbot 低非常多。
(如果使用 userbot 可能登录的那一刻就炸号了)
所以个人认为是最稳的方案。

太长不看

点 击 安 装(需要先安装 Tampermonkey)
然后访问网页版 Telegram:
Telegram Web A version

A version 解锁了 媒体资源下载 + 任意文本复制

Telegram Web K version

K version 解锁了 媒体资源下载,并且这个下载是直接下载而非像 MEGA 一样的先缓存到网页的那种。

^ Tampermonkey 可以在浏览器商店自行下载,这里不再提供安装教程。

思路

可以先看看前两篇文章看下我的思路:

原始思路

download & nodownload
首先就是打开媒体资源,右上角会少掉好几个按钮,下载和转发没掉了,我们切入点就是找这附近的代码。

find code 1
目测 zoom-in / zoom-out 代码肯定在 download 附近:
find code 2
我们在F12 -> sources 切各种 js 搜索,最终找到了 3779 如图所示的代码区域(代码会更新,请以实际情况为准) patch
然后左边的 tab 启用 overrides 强行盖本地的资源过去,修改代码就 ok 了

unregister pwa
然后注销掉 pwa session(unregister)如图所示, ctrl + F5 / cmd + shift + R 忽略缓存刷新网页 patch 就生效了。 patch ok
于是下载按钮就正常加载了,功能也是正常的。

当然每隔一段时间就要修改肯定不怎么爽的,于是我们寻找更激进的 hook 办法:

hook 思路

还是老朋友 Object.defineProperty / new Proxy 来 hook 。
我们先看源代码,给刚才找到的判断下个断点看看:
breakpoint 1
(此处 override 的代码需要直接删除,否则跳转不到 webpack 的原始代码)

find raw code 1
于是可以看到在 ../../global/selectors 的代码完成了判断消息是否做保护的操作:

export function selectIsMessageProtected<T extends GlobalState>(global: T, message?: ApiMessage) {
  return Boolean(message && (message.isProtected || selectIsChatProtected(global, message.chatId)));
}

export function selectIsChatProtected<T extends GlobalState>(global: T, chatId: string) {
  return selectChat(global, chatId)?.isProtected || false;
}

export function selectHasProtectedMessage<T extends GlobalState>(global: T, chatId: string, messageIds?: number[]) {
  if (selectChat(global, chatId)?.isProtected) {
    return true;
  }

  if (!messageIds) {
    return false;
  }

  const messages = selectChatMessages(global, chatId);

  return messageIds.some((messageId) => messages[messageId]?.isProtected);
}

代码出处我们已经知道了,但是我们能做的有限,因为代码都是闭包(大概?)所以只能采取更激进的办法,在这个例子我们可以知道,带保护的消息被授予了 isProtected 为真的属性,有没有一种办法让任意的对象(Object)的 .isProtected 都为 false 呢?

答案是有的,还是我们的老朋友 Object.defineProperty ,只不过这次我们使用了更全局,更危险的办法来安排,只不过这次没有明确的目标,只能劫持所有的 Object.isProtected 了。

Object.defineProperty(Object.prototype, 'isProtected', {
    get: function () {
        return false;
    },
    set: function () {
        return true;
    },
    configurable: false,
  });

Object.prototype 是 JS 运行时所有 Object 的 prototype ,因此这边 hook 就对应了全局的(和 ABEMA 那次的不太一样)

总结

简单来说就是 js 万物都可以重定义,所以不用太惊讶。