// ==UserScript== // @name 微信、知乎、掘金、简书,贴吧,文章页宽屏,贴吧签到 // @namespace http://tampermonkey.net/ // @version 1.1.3 // @description 微信、知乎、掘金、简书,贴吧,文章页宽屏,贴吧签到(模拟客户端获得更多经验) // @author sakura-flutter // @match https://mp.weixin.qq.com/s* // @match https://zhuanlan.zhihu.com/p/* // @match https://www.zhihu.com/question/* // @match https://juejin.im/post/* // @match https://www.jianshu.com/p/* // @match https://tieba.baidu.com/p/* // @match https://tieba.baidu.com/index.html // @match https://tieba.baidu.com // @grant unsafeWindow // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @connect tieba.baidu.com // @require https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.min.js // @require https://cdn.jsdelivr.net/npm/md5/dist/md5.min.js // @compatible chrome >= 80 // @compatible firefox >= 75 // @downloadURL none // ==/UserScript== /* global Vue MD5 */ (function() { 'use strict' // true|false 开启后会打开日志 const isDebug = false const $ = document.querySelector.bind(document) const $$ = document.querySelectorAll.bind(document) function log(...args) { if (!isDebug) return console.log(...args) } // 主函数 function main() { const sites = checkWebsites() sites.forEach(site => { const hanlder = handlers.get(site) log(site) hanlder && hanlder() }) } // 检查网站 function checkWebsites() { const { origin, pathname } = location const url = origin + pathname // 格式[ ['xx', true|false], ] const sites = [ ['mpWeixin', /mp.weixin.qq.com\/s/.test(url)], ['zhihu', /zhuanlan.zhihu.com\/p\//.test(url) || /zhihu.com\/question\//.test(url)], ['juejin', /juejin.im\/post\//.test(url)], ['jianshu', /jianshu.com\/p\//.test(url)], ['tieba', /tieba.baidu.com\/p\//.test(url)], ['tiebaMain', url.endsWith('tieba.baidu.com/') || url.endsWith('tieba.baidu.com/index.html')], ] // 返回匹配的页面 return sites .filter(item => item[1]) .map(item => item[0]) } // 对应网页要执行的操作操作 const handlers = new Map() /* ===微信文章===start */ handlers.set('mpWeixin', function() { const store = createStore('mpWeixin') function execute() { GM_addStyle(` /* 文章宽屏 */ .rich_media_area_primary_inner { max-width: 100vw !important; } /* 二维码位置 */ #js_pc_qr_code .qr_code_pc { position: fixed; top: 25vh; right: 3vw; } @media screen and (min-width: 1024px) { .rich_media_area_primary_inner { max-width: 75vw !important; } #js_pc_qr_code .qr_code_pc { position: fixed; top: 25vh; right: 3vw; } } `) // 文章图片宽高(仅对大图处理) const imgEls = $$('.rich_media_area_primary_inner img') imgEls.forEach(img => { img.addEventListener('load', () => { // 页面本身对图片有宽高处理,延时后再处理 setTimeout(() => { const width = parseFloat(getComputedStyle(img).width) if (width >= 400) { img.style.cssText += 'width: auto !important; height: auto !important;' } },16) }) }) Toast.info('已宽屏处理') } createWidescreenControl({ store, execute }) }) /* ===微信文章===end */ /* ===知乎===start */ handlers.set('zhihu', function() { const store = createStore('zhihu') function execute() { GM_addStyle(` /* 知乎专栏 */ .Post-NormalMain .Post-Header, .Post-NormalMain>div, .Post-NormalSub>div { width: 65vw; min-width: 690px; } .Post-SideActions { left: calc((100vw - 82vw)/2); } /* 知乎问答 */ .QuestionHeader-content, .QuestionHeader-footer { width: 75vw; min-width: 1000px; margin-left: auto; margin-right: auto; } .QuestionHeader-footer-inner { width: auto; } .QuestionHeader-footer-main { padding-left: 0; } .QuestionHeader-main { width: auto; flex: 1; } .Question-main { width: 75vw; min-width: 1000px; } .Question-main .ListShortcut { flex: 1; } .Question-mainColumn { flex: 1; width: auto; padding-right: 10px; } `) Toast.info('已宽屏处理') } createWidescreenControl({ store, execute }) }) /* ===知乎===end */ /* ===掘金===start */ handlers.set('juejin', function() { const store = createStore('juejin') function execute() { GM_addStyle(` /* 掘金文章 */ @media screen and (min-width: 1300px) { .main-container { max-width: 75vw; } .main-container .main-area { width: calc(100% - 21rem); } } `) Toast.info('已宽屏处理') } createWidescreenControl({ store, execute }) }) /* ===掘金===end */ /* ===简书===start */ handlers.set('jianshu', function() { const store = createStore('jianshu') function execute() { GM_addStyle(` /* 简书文章 */ @media screen and (min-width: 1250px) { [role=main] > div:first-child { flex: 1; width: auto; } } @media screen and (min-width: 1250px) { [role=main] { width: 85vw; } #__next > div:last-child { left: 30px; } } @media screen and (min-width: 1450px) { [role=main] { width: 75vw; } #__next > div:last-child { left: 7vw; } } `) Toast.info('已宽屏处理') } createWidescreenControl({ store, execute }) }) /* ===简书===end */ /* ===贴吧===start */ handlers.set('tieba', function() { const store = createStore('tieba') function execute() { GM_addStyle(` /* 帖子 */ @media screen and (min-width: 1390px) { #container { width: 70vw; } #container > .content { width: 100%; } .nav_wrap, .p_thread, .pb_content, .core_title_wrap_bright, .l_post_bright, .core_reply_wrapper, .l_post_bright .core_reply_wrapper, .pb_footer { width: 100%; } /* 内容区域 */ .pb_content { display: flex; background-size: 100%; } .pb_content::after { content: none; } /* 楼区域 */ .left_section { flex: 1; border-right: 2px solid #e4e6eb; } /* 楼层 */ .l_post_bright { display: flex; } .l_post_bright .d_post_content_main{ width: auto; flex: 1; } /* 右侧悬浮按钮 */ .tbui_aside_float_bar { left: auto; right: 11vw; margin-left: 0; } } `) Toast.info('已宽屏处理') } createWidescreenControl({ store, execute }) }) /* ===贴吧===end */ /* ===贴吧主页===start */ handlers.set('tiebaMain', function() { const store = createStore('tiebaMain') const jQuery = unsafeWindow.jQuery const $moreforumEl = jQuery('#moreforum') // 模拟的app版本 const fakeVersion = '11.8.8.0' // 未登录时删除已有的BDUSS if (!$moreforumEl.length) { delete store.BDUSS return } const ui = new Vue({ template: `

模拟APP

自动签到

`, data() { return { loading: false, isSimulate: false, isReverse: store.is_reverse || false, likeForums: [], } }, computed: { isComplete: { get(){ return store.is_complete || false }, set(val) { store.is_complete = val }, }, diaplayForums() { const { isReverse, likeForums } = this return isReverse ? Object.freeze([...likeForums].reverse()) : likeForums }, counter() { const { likeForums } = this return { total: likeForums.length, sign: likeForums.filter(({ is_sign }) => is_sign).length, } }, }, created() { if (store.is_simulate && store.BDUSS) { this.isSimulate = true } if (this.isComplete) { this.run() } }, methods:{ run() { this.loading = true ;(this.isSimulate ? runByBDUSS : runByWeb)(this).finally(() => { this.loading = false }) }, simulateChange({ target: { checked } }) { store.is_simulate = checked if (!checked) return const { BDUSS } = store const result = window.prompt("请输入F12->Application->Cookies中的BDUSS", BDUSS ? BDUSS : undefined) if (result) { store.BDUSS = result location.reload() } else { this.$nextTick(() => { this.isSimulate = false store.is_simulate = false }) } }, reverseChange() { this.isReverse = !this.isReverse store.is_reverse = this.isReverse }, setLikeForums(forums) { this.likeForums = Object.freeze([...forums]) }, updateLikeForum(fid, forum) { const { likeForums } = this const index = likeForums.findIndex(item => +fid === +item.forum_id) if (index === -1) return const target = { ...likeForums[index], ...forum, is_sign: true, } if (forum.sign_bonus_point) { target.user_exp = Number(target.user_exp) + Number(forum.sign_bonus_point) } const ectype = [...likeForums] ectype.splice(index, 1, target) this.likeForums = Object.freeze(ectype) }, // 未签到的靠前 checkUnsign() { const ectype = [...this.likeForums] ectype.sort((a, b) => { if (!a.is_sign && b.is_sign) return -1 return 0 }) this.likeForums = Object.freeze(ectype) }, }, }).$mount() document.body.appendChild(ui.$el) // 模拟APP参数 function makeFakeParams(obj) { return Object.assign({ // 以下可选参数 为了模拟更加真实 _client_type: 4, // 不要更改 _client_version: fakeVersion, _phone_imei: '0'.repeat(15), model: 'HUAWEI P40', // HUAWEI加油 ヾ(◍°∇°◍)ノ゙ net_type: 1, stErrorNums: 1, stMethod: 1, stMode: 1, stSize: 320, stTime: 117, stTimesNum: 1, timestamp: Date.now(), }, obj) } // 贴吧参数签名函数 isFake true时会加入模拟APP参数 function signature(payload, isFake = true) { if (isFake) { payload = makeFakeParams(payload) } // 提交内容所有name-value按照name的字典序升序排列 const sortKeys = Object.keys(payload).sort() // 所有内容按照key=value拼接 let str = sortKeys.reduce((acc, key) => (acc += `${key}=${payload[key]}`), '') // 拼接后补充 str += 'tiebaclient!!!' // 最后以UTF-8编码进行MD5 return MD5(str) } // 界面上无法获得失效的贴吧,这里调用接口获取所有关注的贴吧 async function getLikeForums() { const { BDUSS } = store const tbs = unsafeWindow.PageData && unsafeWindow.PageData.tbs const req2 = makeFakeParams({ BDUSS, tbs, }) const [ like1, like2Map ] = await Promise.all([ request.post('/mo/q/newmoindex').then(response => response.json()).then(data => data.data.like_forum), GMRequest.post('http://c.tieba.baidu.com/c/f/forum/like', utils.URL.stringify({ ...req2, sign: signature(req2), }), { headers: { 'User-agent': `bdtb for Android ${fakeVersion}`, 'Accept': '', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Encoding': 'gzip', 'Cookie': 'ka=open', } }).then(data => data.forum_list).then(forum_list => forum_list.reduce((acc, val) => (acc[val.id] = val, acc), {})), ]) // 融合数据 like1.forEach(forum => { const { forum_id } = forum const like2Forum = like2Map[forum_id] if (!like2Forum) return Object.assign(forum, { levelup_score: like2Forum.levelup_score, level_name: like2Forum.level_name, slogan: like2Forum.slogan, }) }) // 经验降序 like1.sort((a, b) => b.user_exp - a.user_exp) return like1 } if (store.BDUSS) { getLikeForums().then(ui.setLikeForums).then(ui.checkUnsign) } // 通过BDUSS签到 获得经验与客户端签到相同 async function runByBDUSS(ui) { // 贴吧必须先触发才能获取剩下贴吧 $moreforumEl.trigger(new MouseEvent('mouseenter')) // 侧边元素 const likeUnsignEls = $$('#likeforumwraper .unsign') // 查看更多元素 const alwayUnsignEls = $$('#alwayforum-wraper .unsign') // 关闭面板 $moreforumEl.trigger(new Event('click')) const allUnsignEls = [...likeUnsignEls, ...alwayUnsignEls] // 需要重新签到元素(失败时尝试重签) const resignEls = [] if (!allUnsignEls.length) { Toast.success('所有贴吧已经签到') return } const toast = Toast.info({ content: '开始签到,请等待', duration: 0, }) // 签到 function doSign(data) { const { BDUSS } = store const { tbs, fid, kw } = data const params = makeFakeParams({ // 以下4个参数 + 下面sign参数 是必选的 BDUSS, tbs, fid, kw, }) return GMRequest.post('http://c.tieba.baidu.com/c/c/forum/sign', utils.URL.stringify({ ...params, sign: signature(params), }), { headers: { 'User-agent': `bdtb for Android ${fakeVersion}`, 'Accept': '', 'Content-Type': 'application/x-www-form-urlencoded', 'Accept-Encoding': 'gzip', 'Cookie': 'ka=open', } }) } const tbs = unsafeWindow.PageData && unsafeWindow.PageData.tbs while(allUnsignEls.length) { const current = allUnsignEls.shift() const { kw } = utils.URL.parse(current.href) const { fid } = current.dataset const { error_code, error, error_msg, user_info } = await doSign({ tbs, kw, fid }) // 贴吧成功码为0 还会出现code为0但error的情况 if (error_code === '0' && !error) { ui.updateLikeForum(fid, user_info) // 替换已签到样式 current.classList.replace('unsign', 'sign') } else { // 重签 resignEls.push(current) } // 客户端签到可以将延时缩短,随机延时一下 50ms以上 const ms = parseInt(Math.random() * 50 + 50) await utils.sleep(ms) } let failCount = 0 // 重签 while(resignEls.length) { const current = resignEls.shift() const { kw } = utils.URL.parse(current.href) const { fid } = current.dataset const { error_code, error, user_info } = await doSign({ tbs, kw, fid }) if (error_code === '0' && !error) { ui.updateLikeForum(fid, user_info) current.classList.replace('unsign', 'sign') } else { failCount++ Toast.error(`${decodeURIComponent(kw)} 签到失败`) } await utils.sleep(500) } toast.close() failCount ? Toast.warning({ content: `签到成功,失败${failCount}个`, duration: 0, }) : Toast.success('签到成功') ui.checkUnsign() } // 网页签到 经验没客户端那么多 但不需要获得BDUSS只需贴吧已登录即可 async function runByWeb() { // 贴吧必须先触发才能获取剩下贴吧 $moreforumEl.trigger(new MouseEvent('mouseenter')) // 侧边元素 const likeUnsignEls = $$('#likeforumwraper .unsign') // 查看更多元素 const alwayUnsignEls = $$('#alwayforum-wraper .unsign') // 关闭面板 $moreforumEl.trigger(new Event('click')) const allUnsignEls = [...likeUnsignEls, ...alwayUnsignEls] // 需要重新签到元素(失败时尝试重签) const resignEls = [] if (!allUnsignEls.length) { Toast.success('所有贴吧已经签到') return } const toast = Toast.info({ content: '开始签到,请等待', duration: 0, }) // 签到 function doSign(data) { return request.post('/sign/add', { ie: 'utf-8', ...data, }, { headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', } }).then(response => response.json()) } while(allUnsignEls.length) { const current = allUnsignEls.shift() const { kw } = utils.URL.parse(current.href) const { no } = await doSign({ kw }) // 贴吧成功码为0 if (no === 0) { // 替换已签到样式 current.classList.replace('unsign', 'sign') } else { // 重签 resignEls.push(current) } // 网页签到不能太短,否则很容易出现验证码(ಥ﹏ಥ) 验证码2150040 const ms = parseInt(Math.random() * 500 + 500) await utils.sleep(ms) } let failCount = 0 // 重签 while(resignEls.length) { const current = resignEls.shift() const { kw } = utils.URL.parse(current.href) const { no } = await doSign({ kw }) if (no === 0) { current.classList.replace('unsign', 'sign') } else { failCount++ Toast.error(`${decodeURIComponent(kw)} 签到失败`) } await utils.sleep(500) } toast.close() failCount ? Toast.warning({ content: `签到成功,失败${failCount}个`, duration: 0, }) : Toast.success('签到成功') } }) /* ===贴吧主页===end */ // GM请求 function GMRequest(url, options) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ ...options, url, onload(res) { try { resolve(JSON.parse(res.response)) } catch (e) { resolve(res.response) } }, onerror: reject, }); }) } GMRequest.post = function(url, data, options) { return GMRequest(url, { ...options, data, method: 'POST', }) } // 请求 function request(url, options) { return fetch(url, { ...options, }) } request.post = function(url, data, options = {}) { options.headers = Object.assign({}, options.headers) if (data) { let body = data if (options.headers['Content-Type'].includes('application/x-www-form-urlencoded') && Object.prototype.toString.call(data) === '[object Object]') { body = utils.URL.stringify(data) } if (options.headers['Content-Type'].includes('application/json') && Object.prototype.toString.call(data) === '[object Object]') { body = JSON.stringify(data) } options.body = body } return request(url, { ...options, method: 'POST', }) } // 存储 以网站作为模块 function createStore(sitename) { if (!sitename) throw new TypeError('缺少sitename,期望') const getRealProp = property => `${sitename}_${property}` const target = {} const handler = { get(target, property) { const realProp = getRealProp(property) let value = target[realProp] if (value == null) { value = GM_getValue(realProp) target[realProp] = value } return value }, set(target, property, value) { const realProp = getRealProp(property) target[realProp] = value GM_setValue(realProp, value) return true }, deleteProperty(target, property) { const realProp = getRealProp(property) const deleted = delete target[realProp] GM_deleteValue(realProp) return deleted }, } const store = new Proxy(target, handler) return store } // 工具 const utils = { // url解析 URL: { parse() {}, stringify() {}, }, // 转formdata toFormData() {}, // 延时 async sleep() {}, } utils.URL.parse = function(string) { const url = new URL(string) const searchParams = new URLSearchParams(url.search) return [...searchParams.entries()].reduce((acc, [key, value]) => (acc[key] = value, acc), {}) } utils.URL.stringify = function(obj) { return Object.entries(obj).map(([key, value]) => `${key}=${value}`).join('&') } utils.toFormData = function(params = {}) { const formData = new FormData() for (const [key, value] of Object.entries(params)) { formData.append(key, value) } return formData } utils.sleep = ms => new Promise(resolve => setTimeout(resolve, ms)) // Toast 可以直接Toast[type] 调用 function Toast(options) { if (typeof options === 'string') { options = { content: options } } // 参数 options = Object.assign({ content: '', type: 'info', duration: 3000, // 0不会自动关闭 }, options) const toast = new Vue({ template: `
`, data() { return { content: options.content, type: options.type, visible: true, } }, computed: { // 颜色 colour() { switch(this.type) { case 'info': return { color: '#2e8bf0', background: '#f0faff', border: '1px solid #d4eeff', } case 'success': return { color: '#19bf6c', background: '#edfff3', border: '1px solid #bbf2cf', } case 'warning': return { color: '#f90', background: '#fff9e6', border: '1px solid #ffe7a3', } case 'error': return { color: '#ed3f13', background: '#ffefe6', border: '1px solid #ffcfb8', } } } }, methods: { // export-api // 关闭 close() { this.visible = false }, beforeEnter(el) { el.style.opacity = 0 el.style.transform = 'translate(-50%, -10%)' }, enter(el, done) { setTimeout(() => { el.style.opacity = 1 el.style.transform = 'translate(-50%, 0)' }) }, leave(el, done) { setTimeout(() => { el.style.opacity = 0 el.style.transform = 'translate(-50%, 30%)' }) }, afterLeave () { this.$destroy() this.$el.parentNode.removeChild(this.$el) }, }, }).$mount() document.body.appendChild(toast.$el) if (options.duration > 0) { setTimeout(() => { toast.visible = false }, options.duration) } return { // 关闭 close: toast.close, } } ['info', 'success', 'warning', 'error'].forEach(type => { Toast[type] = function(options) { if (typeof options === 'string') { options = { content: options } } options = { ...options, type, } return Toast(options) } }) // 宽屏开关 options: store, execute要执行的函数 function createWidescreenControl(options) { const { store, execute } = options const buttonComponent = new Vue({ template: ` `, data() { return { isOpen: store.is_open || false, } }, beforeCreate() { store.is_open && execute() }, methods: { async toggle() { store.is_open = !this.isOpen location.reload() } }, }).$mount() document.body.appendChild(buttonComponent.$el) } main() })();