// ==UserScript==
// @name showBossActiveTime
// @namespace http://www.chensong.cc/
// @version 0.4.3
// @description to show hr lastest login time,help you deliver your resume efficiently.
// @author chensong
// @match https://www.zhipin.com/web/geek/job
// @match https://www.zhipin.com/web/geek/job*
// @match https://www.zhipin.com/web/geek/recommend
// @match https://www.zhipin.com/web/geek/recommend*
// @exclude https://www.zhipin.com/web/geek/map/jobs*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_xmlhttpRequest
// @license MIT
//失业中,欢迎来扰:) 857763541@qq.com
// ==/UserScript==
(function () {
'use strict';
class ShowBossActiveTime {
static MAXLIMIT = 5;
static TIME = 2000;
constructor(options) {
this.queryQueue = []; //记录需要查询的队列
this.currentDomList = []; //当前列表的dom
this.secondQueryList = []; //需要二次查询的队列
this.maxLimit = ShowBossActiveTime.MAXLIMIT;
this.timer = null;
this.frame = null;
let bossActiveStatusList;
try {
bossActiveStatusList = JSON.parse(
localStorage.getItem('bossActiveStatusList')
) || [
'半年前活跃',
'近半年活跃',
'4月前活跃',
'2月内活跃',
'2周内活跃'
];
} catch (error) {
// 转换之前客户端存在的数据格式
bossActiveStatusList = localStorage
.getItem('bossActiveStatusList')
.split(',');
}
this.statusOptions = bossActiveStatusList.filter((option)=> option && option!== '');
this.removeStatusList = [];
this.options = Object.assign(
{
listElement: '.job-card-wrapper',
onlineElement: '.boss-online-tag',
chatElement: '.start-chat-btn',
hunterElement: '.job-tag-icon',
linkElement: '.job-card-left',
paginationElement: '.options-pages',
hideChated: false
},
options
)
this.list = []; //数据列表
//添加过滤条件,因为要保存选择数据,所以这个不能切换时清空
this.addStatusFilter();
this.addStyleSheet();
this.ready();
// 监听请求数据事件
this.observeLoadingData();
this.init();
}
// 获取节点列表
getList() {
Array.from(document.querySelectorAll(this.options.listElement)).forEach(
(node) => {
const status = node.querySelector(this.options.onlineElement);
// 保存所有list的dom
this.list.push(node);
let hunter = node.querySelector(this.options.hunterElement);
hunter && (hunter = hunter.alt === '猎头')
// 不在线且不是猎头
if (!status && !hunter) {
// 需要查询的dom
this.queryQueue.push(node);
}else{
//如果是猎头
if (hunter) {
const chat = node.querySelector(this.options.chatElement).textContent;
this.setText(node, '猎头,没有活跃状态', chat);
return;
}
// 先给在线的数据设置状态
const chat = node.querySelector(this.options.chatElement).textContent;
const online = node.querySelector(this.options.onlineElement);
if (online) {
this.setText(node, '在线', chat);
}
}
}
);
}
getListStatus() {
// 每两秒下一个请求
// 保存请求的条数
let requestNum = this.queryQueue.length;
let requestedNum = 0;
this.timer = setInterval(() => {
if (this.queryQueue.length === 0) {
clearInterval(this.timer);
this.timer = null;
this.maxLimit = ShowBossActiveTime.MAXLIMIT;
return;
}
if (this.maxLimit > 0) {
this.maxLimit--;
let node = this.queryQueue.shift();
let link = node.querySelector(this.options.linkElement).href;
let chat = node.querySelector(this.options.chatElement).textContent;
this.getStatusByXHR(link, node, chat).then(({ text, type }) => {
// 设置文字
if(text===''){
text ='没有找到hr登录状态'
}
this.setText(node, text, chat);
this.toggleDom(node);
console.log(type, text, link);
// 增加一个请求
this.maxLimit++;
requestedNum++;
if (requestedNum === requestNum) {
this.alertBox('查询完毕');
}
});
}
}, ShowBossActiveTime.TIME);
}
async getStatusByXHR(url, node, chat) {
return new Promise((resolve) => {
// 第一次获取
GM_xmlhttpRequest({
method: 'GET',
url,
headers: {
Cookie: document.cookie
},
onload: (response) => {
if (/security-check.html/.test(response.finalUrl)) {
// 出现重定向后,使用返回的callbackUrl生成目标链接,二次获取
this.getHtmlByFormatUrl(response, 1).then((res) => {
resolve(res);
});
} else {
const doc = this.parseHtml(response);
const text = this.getStatusText(doc);
resolve({
text,
type: '第一次就获取到的数据'
});
}
}
});
});
}
getHtmlByFormatUrl(response, repeatCount) {
return new Promise((resolve) => {
// 二次获取,说明触发302,需要延时
setTimeout(() => {
getFinalUrl(response.finalUrl, (url) => {
GM_xmlhttpRequest({
method: 'GET',
url,
onload: (res) => {
if (!/security-check.html/.test(res.finalUrl)) {
const doc = this.parseHtml(res);
const text = this.getStatusText(doc);
resolve({
text,
type: '二次请求的数据'
});
} else {
// 二次请求不成功,继续使用GM_xmlhttpRequest成功率低,转为其它形式,先fetch,fetch不行,用iframe
return fetch(url, { redirect: 'error' })
.then((res) => {
return res.text();
})
.then(async (data) => {
const doc = document.createElement('div');
doc.insertAdjacentHTML('afterbegin', data);
const text = this.getStatusText(doc);
resolve({
text,
type: 'fetch的数据'
});
})
.catch(async (error) => {
/*请求被302临时重定向了,无法获取到数据,需要用iframe来获取了*/
this.getStatusByIframe(url).then((text) => {
resolve({
text,
type: 'iframe的数据'
});
});
});
}
}
});
});
}, 1000 * repeatCount);
});
}
parseHtml(res) {
const html = res.responseText;
const parser = new DOMParser();
return parser.parseFromString(html, 'text/html');
}
ready() {
var frame = document.createElement('iframe');
frame.style.height = 0;
frame.style.width = 0;
frame.style.margin = 0;
frame.style.padding = 0;
frame.style.border = '0 none';
frame.name = 'zhipinMyFrame';
frame.src = 'about:blank';
(document.body || document.documentElement).appendChild(frame);
this.frame = frame;
}
checkIsSecurityCheckPage(tempIframe) {
return tempIframe.contentWindow.securityPageName === 'securityCheck';
}
async getStatusByIframe(url) {
let iframe = document.createElement('iframe');
let match = url.match(/job_detail\/([^\/]+)\.html/);
iframe.src = url;
iframe.id = 'tempIframe' + match[1];
iframe.style.cssText = 'width:0;height:0;';
document.body.appendChild(iframe);
return await new Promise((resolve) => {
// 定时检测iframe中的某个节点是否已经渲染
let timer = setInterval(() => {
// 获取iframe中的document对象
let iframeDoc =
iframe.contentDocument || iframe.contentWindow.document;
let hasFooter = iframeDoc.querySelector('#footer');
let status = this.getStatusText(iframeDoc);
let verify = iframeDoc.querySelector('page-verify-slider') || document.querySelector('body').textContent.match(/您的账号可能存在异常访问行为,完成验证后即可正常使用/g)
if(verify){
this.alertBox('更新过于频繁,触发认证操作了');
this.clear();
}
if (hasFooter && iframeDoc.title !== '请稍候' && status) {
resolve(status);
iframe.remove();
clearInterval(timer);
}
}, 200);
});
}
observeLoadingData() {
const container = document.querySelector('.search-job-result');
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
const addNode = mutation.addedNodes;
const removedNode = mutation.removedNodes;
if (
addNode.length &&
addNode[0].className === 'job-loading-wrapper'
) {
console.log('触发了请求列表数据');
}
if (
removedNode.length &&
removedNode[0].className === 'job-loading-wrapper'
) {
console.log('页面加载完成,开始执行查询状态');
this.clear();
this.init();
}
}
});
});
const config = { attributes: false, childList: true, subtree: false };
observer.observe(container, config);
// 监听是否是搜索列表页,不是就移除dom
const listContainer = document.querySelector('#wrap');
const listObserver = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
const wrapper = document.querySelector('.job-list-wrapper');
const removeNode = document.querySelector(
'#removeFilterDataContainer'
);
if (!wrapper && removeNode) {
document.body.removeChild(removeNode);
listObserver.disconnect();
// 清除查询
this.clear();
}
}
});
});
listObserver.observe(listContainer, config);
}
alertBox(msg) {
let div = document.createElement('div');
div.id = 'alertBox';
div.innerHTML = msg;
document.body.appendChild(div);
setTimeout(function () {
document.body.removeChild(div);
}, 2000);
}
getStatusText(doc) {
const timeNode = doc.querySelector('.boss-active-time');
if (timeNode) {
return timeNode.textContent;
} else {
let status = null;
// 没有获取到状态,但页面是已经加载到的了
if (doc.querySelector('.job-boss-info')) {
status = '获取到数据了,但不知道是什么数据';
}
const isHunter = ['.certification-tags', '.boss-info-attr'].filter(
(name) => {
const node = doc.querySelector(name);
return /猎头|人力|人才|经纪/.test(node?.textContent);
}
);
isHunter && (status = '猎头,没有活跃状态');
return status;
}
}
toggleDom(node) {
const status = node.querySelector('.status')?.textContent;
const chat = node.querySelector(this.options.chatElement).textContent;
// 先显示全部
node.style.display = 'block';
// 首先判断是否隐藏已沟通
if (this.options.hideChated && chat === '继续沟通') {
node.style.display = 'none';
}
// 状态数据已经获取了
if (status && chat) {
if (this.removeStatusList.includes(status)) {
node.style.display = 'none';
}
if (this.options.hideChated && chat === '继续沟通') {
node.style.display = 'none';
}
}
}
toggleDoms() {
this.list.forEach((node) => {
this.toggleDom(node);
});
}
addStatusFilter() {
const container = document.createElement('div');
container.id = 'removeFilterDataContainer';
const html = `
<label><input type="checkbox" name="hideChated" value="1">过滤已经沟通过的</label>
<div id="boss-active-time-arrow"><svg stroke="currentColor" fill="none" stroke-width="2" viewBox="0 0 24 24" aria-hidden="true" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"></path></svg></div>
`;
const title = document.createElement('div');
title.className = 'title';
title.innerHTML = html;
const tips = document.createElement('div');
tips.innerHTML = '过滤掉勾选的数据';
tips.className = 'tips';
container.appendChild(title);
container.appendChild(tips);
container
.querySelector('#boss-active-time-arrow')
.addEventListener('click', function () {
container.classList.contains('hide')
? container.classList.remove('hide')
: container.classList.add('hide');
});
this.statusOptions.forEach((option) => {
const label = document.createElement('label');
const el = document.createElement('input');
el.type = 'checkbox';
el.name = option;
el.value = option;
el.className = 'status-checkbox';
label.appendChild(el);
label.appendChild(document.createTextNode(option));
container.appendChild(label);
});
container.addEventListener('change', () => {
const selectedValues = Array.from(
container.querySelectorAll('.status-checkbox:checked')
).map((el) => el.value);
this.removeStatusList = selectedValues;
const hideNode = document.querySelector('input[name="hideChated"]');
this.options.hideChated = hideNode?.checked;
this.toggleDoms();
});
document.body.appendChild(container);
}
// 清空查询列表,清除缓存的dom
clear() {
this.queryQueue.length = 0;
this.list.length = 0;
this.currentDomList.length = 0;
this.maxLimit = ShowBossActiveTime.MAXLIMIT;
clearInterval(this.timer);
this.timer = null;
}
ready() {
let frame = document.createElement('iframe');
frame.style.height = 0;
frame.style.width = 0;
frame.style.margin = 0;
frame.style.padding = 0;
frame.style.border = '0 none';
frame.name = 'zhipinMyFrame';
frame.src = 'about:blank';
document.body.appendChild(frame);
}
addStyleSheet() {
const style = `
.show-active-status{display:flex;padding:5px 10px;background:#e1f5e3;color:green;width:80%;border-radius:4px;margin-top:10px;margin-bottom:5px;}
.show-active-status .status{}
.show-active-status .chat{}
#alertBox{position: fixed; top: 20%; left: 50%; transform: translate(-50%, -50%); background-color: rgb(0 190 189); border-radius: 5px; color: #fff; z-index: 9999; padding: 20px 40px; font-size: 20px; box-shadow: 0px 0px 10px rgba(0,0,0,.2);}
#removeFilterDataContainer{
position: fixed;right: 70px;top: 70px;z-index: 20000;background: #00bebd; color: #fff;display: flex;flex-direction: column;padding-bottom:10px
}
#removeFilterDataContainer.hide{height:28px;overflow:hidden}
#removeFilterDataContainer .title {display:flex;justify-content: space-around;}
#removeFilterDataContainer .title label{align-items:center;padding:0 15px;}
#removeFilterDataContainer.hide #boss-active-time-arrow svg{transform: rotate(180deg);}
#removeFilterDataContainer #boss-active-time-arrow {cursor: pointer;font-size: 24px;background: #009796;padding:2px 10px;line-height:1;}
#removeFilterDataContainer .tips{font-size:16px;margin:5px 20px;}
#removeFilterDataContainer label{display:flex;padding:0 20px;}
#removeFilterDataContainer label input{margin-right:5px;}
`;
const styleEle = document.createElement('style');
styleEle.id = 'show-boss-active-time-css';
styleEle.innerHTML = style;
document.head?.appendChild(styleEle);
}
// 设置文本内容
setText(node, text, status) {
const html = `
<div class="show-active-status">
<p class="status">${text}</p>
<p class="chat">${status}</p>
</div>
`;
node.querySelector('.job-info').insertAdjacentHTML('afterend', html);
let aEle = node.querySelector('a');
aEle.style.height = 'auto';
aEle.style.paddingBottom = '0';
// 如果是新的状态值,加入到状态列表中
if (!this.statusOptions.includes(text) && text !== '在线') {
this.statusOptions.push(text);
localStorage.setItem(
'bossActiveStatusList',
JSON.stringify(this.statusOptions)
);
}
}
// 获取列表数据
init() {
this.getList();
this.alertBox('开始更新状态....,网站安全策略问题,更新会比较缓慢。');
this.getListStatus();
// 判断是否要隐藏已经沟通过的数据
this.toggleDoms();
}
}
function start() {
const Lis = document.querySelectorAll('.job-card-wrapper');
if (Lis.length) {
new ShowBossActiveTime();
} else {
console.log('no start');
setTimeout(start, 2000);
}
}
start();
// 获取302后详情页的最终地址
function getFinalUrl(url, cb) {
let securityPageName = 'securityCheck';
const urlObject = new URL(url);
const search = urlObject.search;
(function () {
var image = new Image();
var lenSrcReferer = url.split('srcReferer').length - 1;
image.src =
'https://t.zhipin.com/f.gif?pk=' +
securityPageName +
'&len=' +
lenSrcReferer +
'&r=' +
document.referrer;
})();
(function () {
var pageInterNum = 0;
var pageStartTime = new Date().getTime();
var UA = window.navigator.userAgent;
var isIE;
if (UA.indexOf('MSIE ') > -1) {
isIE = true;
}
let frame = document.querySelector('#zhipinMyFrame');
init(frame, url);
function init(frame) {
var COOKIE_DOMAIN = (function () {
var hostName = location.hostname;
if (hostName === 'localhost' || /^(\d+\.){3}\d+$/.test(hostName)) {
return hostName;
}
return '.' + hostName.split('.').slice(-2).join('.');
})();
var seriesLoadScript = function (scriptUrl, callback) {
var url = scriptUrl;
var script = document.createElement('script');
script.setAttribute('type', 'text/javascript');
script.setAttribute('charset', 'UTF-8');
debugger;
script.onload = script.onreadystatechange = function () {
if (
!isIE ||
this.readyState == 'loaded' ||
this.readyState == 'complete'
) {
callback();
}
};
script.setAttribute('src', scriptUrl);
if (frame.tagName != 'IFRAME') {
frame.appendChild(script);
} else if (frame.contentDocument) {
if (frame.contentDocument.body) {
frame.contentDocument.body.appendChild(script);
} else {
frame.contentDocument.documentElement.appendChild(script);
}
} else if (frame.document) {
if (frame.document.body) {
frame.document.body.appendChild(script);
} else {
frame.document.documentElement.appendChild(script);
}
}
};
var getQueryString = function (name) {
var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)');
var r = search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
};
var Cookie = {
get: function (name) {
var arr,
reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)');
if ((arr = document.cookie.match(reg))) {
return unescape(arr[2]);
} else {
return null;
}
},
set: function (name, value, time, domain, path) {
var str = name + '=' + encodeURIComponent(value);
if (time) {
var date = new Date(time).toGMTString();
str += ';expires=' + date;
}
str = domain ? str + ';domain=' + domain : str;
str = path ? str + ';path=' + path : str;
document.cookie = str;
}
};
var urlFilter = {
config: {
url: '',
whiteHostList: [
'm.zhipin.com',
'www.zhipin.com',
'pre-www.zhipin.com'
],
blackPathList: ['security-check.html', 'security-check1.html']
},
setStrategy: function () {
var url = urlFilter.config.url;
switch (true) {
case urlFilter.isBlackHost(url) || urlFilter.hasBlackPath(url):
urlFilter.config.url = '/';
break;
}
return urlFilter.config.url;
},
isAbsolutePathStartable: function (url) {
return url.indexOf('//') < 0 && url.indexOf('/') === 0;
},
isBlackHost: function (url) {
var isBlackHost = false;
var rule = /^(https?)?(:?\/\/+)([^\/?]*)(.*)?$/;
url.replace(rule, function (res, $1, $2, $3, $4) {
isBlackHost = !urlFilter.isHostInWhiteList($3);
console.error('hostname', $3, 'isBlackHost', isBlackHost);
return isBlackHost ? '/' : url;
});
return isBlackHost;
},
hasBlackPath: function (url) {
var isBlackPath = false;
var blackPathList = urlFilter.config.blackPathList;
for (var i = 0; i < blackPathList.length; i++) {
if (url.indexOf(blackPathList[i]) > -1) {
isBlackPath = true;
break;
}
}
return isBlackPath;
},
isHostInWhiteList: function (hostname) {
return urlFilter.config.whiteHostList.indexOf(hostname) > -1;
},
filter: function (url) {
urlFilter.config.url = url || '/';
return urlFilter.setStrategy();
}
};
var jumpReplace = function (url) {
var filterUrl = urlFilter.filter(url);
if (
filterUrl.indexOf('napi') > -1 &&
filterUrl.indexOf('zpssr') > -1
) {
filterUrl = filterUrl.replace(
/\/napi\/zpssr(site|youle|tools)/,
''
);
}
// console.log(filterUrl);
cb(filterUrl);
};
var jumpPage = function (srcReferer, callbackUrl) {
if (callbackUrl || srcReferer.indexOf('security-check.html') > -1) {
jumpReplace(callbackUrl);
} else {
jumpReplace(srcReferer);
}
var image = new Image();
image.src =
'https://t.zhipin.com/f.gif?pk=' +
securityPageName +
'&ca=securityCheckJump_' +
Math.round((new Date().getTime() - pageStartTime) / 1000) +
'&r=' +
document.referrer;
};
var seed = getQueryString('seed') || '';
var ts = getQueryString('ts') || '';
var fileName = getQueryString('name') || '';
var callbackUrl = getQueryString('callbackUrl') || '';
var srcReferer = getQueryString('srcReferer') || '';
if (fileName === 'null' || !seed || !fileName || !callbackUrl) {
var image = new Image();
var paramsStr = '';
var lenSrcReferer = url.split('srcReferer').length - 1;
var reportData = {
appKey:
url.indexOf('weizhipin') > -1
? 'ed8d1b9d40a89f30ba721'
: 'd071323e4304ae2931f11',
errorType: 'collectData',
errorCode: 'security_check_error',
json: JSON.stringify({
url: url,
fileName: fileName,
seed: seed,
ts: ts,
callbackUrl: callbackUrl,
srcReferer: srcReferer
})
};
for (var key in reportData) {
paramsStr += '&' + key + '=' + reportData[key];
}
paramsStr = paramsStr.substr(1);
image.src = 'https://t.kanzhun.com/z.json?' + paramsStr;
}
if (seed && ts && fileName) {
jumpPage(srcReferer, callbackUrl);
}
}
var ie = !!(window.attachEvent && !window.opera);
var wk = /webkit\/(\d+)/i.test(navigator.userAgent) && RegExp.$1 < 525;
var fn = [];
var run = function () {
for (var i = 0; i < fn.length; i++) fn[i]();
};
})();
}
})();