filters.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196
  1. /**
  2. * filters.js — 「只要这样的账号」的判定
  3. *
  4. * 这一层是**纯函数**:给它一份采集到的资料 + 一份过滤设置,它只回答"要 / 不要 / 为什么"。
  5. * 不点屏幕、不读写文件、不知道 TikTok 长什么样。所以它能被 100% 离线自检,
  6. * 而"界面上的 21 项设置真的会拦人"这件事也就有了证据。
  7. *
  8. * ## 一条重要的区分:**数据缺失**不等于**不符合**
  9. * 三种"没有值"必须分开处理,否则过滤要么形同虚设,要么乱杀:
  10. *
  11. * ① 用户没设这个门槛 → 放行(设置里是 0,语义就是"不限")
  12. * ② 设了,但平台根本不提供 → **放行 + 记一条 warn**(例如"视频量":真机实测用户页
  13. * 顶部只有 关注/粉丝/获赞 三组数字,没有视频数)
  14. * ③ 设了,本应能读到却读不到 → **拦下**(读不到就无法证明符合,宁可少收)
  15. *
  16. * ②和③看起来像,但责任不同:②是我们做不到,不该惩罚用户抓到的人;③是数据异常,
  17. * 放行会让过滤失去意义而用户以为它生效了。
  18. *
  19. * 依赖:TikTokAuto.parse、TikTokAuto.logger(调用时取)
  20. */
  21. TikTokAuto.filters = (function () {
  22. 'use strict';
  23. /**
  24. * 这份过滤设置是否需要打开用户页才能判。
  25. *
  26. * 用途是**快路径**:所有过滤都关着时(最常见的第一种用法),
  27. * 直接从列表页把 uid 抄下来就行 —— 不必为每个人开一次用户页。
  28. * 实测一次用户页打开约 3~5 秒,100 个人就是 5~8 分钟,差一个数量级。
  29. *
  30. * 注意 `videos` 不参与这个判断:它读不到(见文件头 ②),
  31. * 为此打开用户页也只是白花时间。
  32. */
  33. function needsProfile(filter) {
  34. var f = filter || {};
  35. if (f.private === true || f.noAvatar === true) {
  36. return true;
  37. }
  38. if (f.langRegion && f.langRegion !== 'any') {
  39. return true;
  40. }
  41. var groups = ['follow', 'fans', 'likes'];
  42. for (var i = 0; i < groups.length; i += 1) {
  43. var range = f[groups[i]] || {};
  44. if (!TikTokAuto.parse.isRangeOpen(range.min, range.max)) {
  45. return true;
  46. }
  47. }
  48. return false;
  49. }
  50. /** 取该判定的取样文本:昵称、个签,或两者(昵称检测下拉的三个取值)。 */
  51. function sampleText(profile, mode) {
  52. var nick = profile.nickname || '';
  53. var sign = profile.signature || '';
  54. if (mode === 'nick') {
  55. return nick;
  56. }
  57. if (mode === 'sign') {
  58. return sign;
  59. }
  60. /* "通杀":两段合起来判 —— 分开判会让"昵称中文、个签全英文"的人两边都不算 */
  61. return nick + ' ' + sign;
  62. }
  63. /**
  64. * 单条判定的结果。
  65. * `soft` 表示"我们做不到,放行但要告知"(对应文件头的 ②)。
  66. */
  67. function pass() {
  68. return { ok: true, reason: '', soft: '' };
  69. }
  70. function reject(reason) {
  71. return { ok: false, reason: reason, soft: '' };
  72. }
  73. function softPass(reason) {
  74. return { ok: true, reason: '', soft: reason };
  75. }
  76. /**
  77. * 判定一个账号要不要。
  78. *
  79. * @param profile 采集到的资料;`null` 表示"这次没有打开用户页"(快路径)
  80. * —— 此时只有不看资料就能判的项目生效,其余全部放行
  81. * @param filter 过滤设置
  82. * @returns { ok, reason, soft }
  83. * reason 是"为什么不要"(人话,会进日志)
  84. * soft 是"放行了但你应该知道"(进 summary 的 warnings,只报一次)
  85. */
  86. function judge(profile, filter) {
  87. var f = filter || {};
  88. var parse = TikTokAuto.parse;
  89. if (!profile) {
  90. /* 没有资料,两种情况必须分开:
  91. * · 这份设置**本来就不需要**资料(快路径)→ 放行;
  92. * · 需要资料却拿不到(用户页打不开、读不出)→ **拦下**。
  93. * 第二种如果放行,等于"打不开的人全都通过" —— 过滤看着在跑,
  94. * 实际放进来一批根本没检查过的账号,比不过滤更糟(用户以为筛过了)。 */
  95. if (needsProfile(f)) {
  96. return reject('打不开用户页,读不到任何资料');
  97. }
  98. return pass();
  99. }
  100. /* ① 私密账户。列表页读不到,只能打开用户页看提示文案。 */
  101. if (f.private === true && profile.isPrivate === true) {
  102. return reject('私密账号');
  103. }
  104. /* ② 无头像。真机实测头像节点不可读(无文案、不可点),
  105. * 只能靠像素法 —— 见 profile.js 里的说明。读不到时放行并告知。 */
  106. if (f.noAvatar === true) {
  107. if (profile.hasAvatar === false) {
  108. return reject('无头像');
  109. }
  110. if (profile.hasAvatar === null) {
  111. return {
  112. ok: true,
  113. reason: '',
  114. soft: '无头像检测:头像读取不可用,本次未生效'
  115. };
  116. }
  117. }
  118. /* ③ 语言区域:按昵称/个签的字符构成判(见 parse.language 的规则说明) */
  119. if (f.langRegion && f.langRegion !== 'any') {
  120. var text = sampleText(profile, f.nicknameCheck || 'any');
  121. var lang = parse.language(text);
  122. if (lang === 'none') {
  123. /* 纯 emoji / 纯数字昵称:没有语言线索。用户明确要筛语言,就不该混进来。 */
  124. return reject('看不出语言(昵称与个签都没有文字)');
  125. }
  126. if (!parse.languageMatches(f.langRegion, lang)) {
  127. return reject('语言不符:' + lang + ' ≠ ' + f.langRegion);
  128. }
  129. }
  130. /* ④ 三组数量门槛。真机实测用户页顶部是 关注 / 粉丝 / 获赞,
  131. * 数字形态是缩写("1.6M"),parse.count 负责解析。 */
  132. var checks = [
  133. { key: 'follow', label: '关注量' },
  134. { key: 'fans', label: '粉丝量' },
  135. { key: 'likes', label: '获赞量' }
  136. ];
  137. for (var i = 0; i < checks.length; i += 1) {
  138. var one = checks[i];
  139. var range = f[one.key] || {};
  140. if (parse.isRangeOpen(range.min, range.max)) {
  141. continue;
  142. }
  143. if (!parse.inRange(profile[one.key], range.min, range.max)) {
  144. var actual = profile[one.key];
  145. return reject(one.label + '不在区间内(' +
  146. (actual === null || actual === undefined ? '读不到' : actual) + ')');
  147. }
  148. }
  149. /* ⑤ 视频量:平台不提供(实测),放行 + 告知。见文件头 ②。 */
  150. var videoRange = f.videos || {};
  151. if (!parse.isRangeOpen(videoRange.min, videoRange.max)) {
  152. return softPass('视频量:用户页不显示该数字,本次未参与过滤');
  153. }
  154. return pass();
  155. }
  156. /** 给人看的一句话:这次到底在筛什么(进日志与界面状态条) */
  157. function describe(filter) {
  158. var f = filter || {};
  159. var parts = [];
  160. if (f.private === true) { parts.push('排除私密'); }
  161. if (f.noAvatar === true) { parts.push('排除无头像'); }
  162. if (f.langRegion && f.langRegion !== 'any') {
  163. parts.push(f.langRegion === 'zh' ? '仅中文' : '仅英文');
  164. }
  165. var labels = { follow: '关注', fans: '粉丝', likes: '获赞' };
  166. var keys = ['follow', 'fans', 'likes'];
  167. for (var i = 0; i < keys.length; i += 1) {
  168. var range = f[keys[i]] || {};
  169. if (TikTokAuto.parse.isRangeOpen(range.min, range.max)) {
  170. continue;
  171. }
  172. parts.push(labels[keys[i]] + ' ' + (range.min || 0) + '~' + (range.max || '∞'));
  173. }
  174. return parts.length === 0 ? '不过滤(拿到就收)' : parts.join(',');
  175. }
  176. return {
  177. needsProfile: needsProfile,
  178. judge: judge,
  179. describe: describe
  180. };
  181. }());