Greasy Fork

Greasy Fork is available in English.

百度贴吧签到

网页版签到或模拟客户端签到,模拟客户端可获得与客户端相同经验并且签到速度更快~

当前为 2020-09-14 提交的版本,查看 最新版本

// ==UserScript==
// @name         百度贴吧签到
// @namespace    http://tampermonkey.net/
// @version      2.0.0
// @description  网页版签到或模拟客户端签到,模拟客户端可获得与客户端相同经验并且签到速度更快~
// @author       sakura-flutter
// @run-at       document-end
// @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_deleteValue
// @grant        GM_addStyle
// @connect      tieba.baidu.com
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js
// @require      https://cdn.jsdelivr.net/npm/md5/dist/md5.min.js
// @require      http://greasyfork.icu/scripts/411093-toast/code/Toast.js?version=847261
// @compatible   chrome >= 80
// @compatible   firefox >= 75
// ==/UserScript==

/* global Vue MD5 Toast */

(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 store = createStore()
			const jQuery = unsafeWindow.jQuery
			const $moreforumEl = jQuery('#moreforum')
			// 模拟的app版本
			const fakeVersion = '11.8.8.0'
			// 未登录时删除已有的BDUSS
			if (!$moreforumEl.length) {
					delete store.BDUSS
					delete store.tiebaMain_BDUSS
					delete store.is_complete
					return
			}

			const ui = new Vue({
					template: `
						 <div>
							 <div style="position:fixed; z-index:500; top:80px; right:150px;">
								 <button
										style="padding:10px; font-size:14px; color:#fff; background:#3385ff; box-shadow:0 1px 6px rgba(0,0,0,.2);"
										:disabled="loading"
										@click="run"
								 >
									 一键签到
								 </button>
								 <p style="margin-top:10px; text-align:center;" title="模拟APP签到可以获得与APP相同的经验,比网页签到经验更多,也提供更多功能,但需要BDUSS,重新登录后需要再次输入,请网上搜索获得方法,不勾选则通过网页签到,此时不需要BDUSS">
									 <input style="vertical-align:text-top;" v-model="isSimulate" type="checkbox" @change="simulateChange" /> 模拟APP
								 </p>
								 <p style="text-align:center;" title="下次进入贴吧时自动签到,建议同时勾选模拟APP">
									 <input style="vertical-align:text-top;" v-model="isComplete" type="checkbox" /> 自动签到
								 </p>
							 </div>
							 <div style="position:fixed; z-index:2; top:200px; right:10px; width:19vw; min-width:280px; box-shadow:0 1px 6px rgba(0,0,0,.2); background:#fafafa; padding:5px;" v-if="likeForums.length">
									<button style="display:block; text-align:center; width:100%;" @click="reverseChange">{{isReverse ? '已倒序' : '普通'}}  <span title="已签/总数">{{counter.sign}}/{{counter.total}}</span></button>
									<ul style="max-height:65vh; overflow-x:hidden;">
										<li style="display:flex; border-bottom:1px solid rgba(221, 221, 221, .5);" v-for="item in diaplayForums" :key="item.forum_id">
											<span style="width:56px;" :title="item.level_name">{{item.user_level}}级{{item.is_sign ? ' √' : ''}}{{item.sign_bonus_point ? ('+' + item.sign_bonus_point) : ''}}</span>
											<a style="flex:1; overflow:hidden; white-space:nowrap; text-overflow:ellipsis;" :href="'/f?kw=' + item.forum_name" :title="item.forum_name" target="_blank">{{item.forum_name}}</a>
											<span style="width:80px" :title="'距离升级' + (item.levelup_score - item.user_exp)">{{item.user_exp}}/{{item.levelup_score}}</span>
										</li>
									</ul>
								</div>
						 </div>
					 `,
					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
					const queue = new Queue({
							tasks: allUnsignEls.map(current => {
									return async function () {
											const { kw } = utils.URL.parse(current.href)
											const { fid } = current.dataset
											const { error_code, error, 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() * 20 + 50)
											await utils.sleep(ms)
									}
							})
					})
					await queue.run()

					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('签到成功')
			}
	}

	// 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() {
			const target = {}
			const handler = {
					get(target, property) {
							let value = target[property]
							if (value == null) {
									value = GM_getValue(property)
									target[property] = value
							}
							// 兼容之前版本tiebaMain_BDUSS字段
							if (property === 'BDUSS' && value == null) {
									value = GM_getValue('tiebaMain_BDUSS')
									if (value) {
											store[property] = value
									}
							}
							return value
					},
					set(target, property, value) {
							target[property] = value
							GM_setValue(property, value)
							return true
					},
					deleteProperty(target, property) {
							const deleted = delete target[property]
							GM_deleteValue(property)
							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))

	class Queue {
			// 默认同时进行6个任务
			constructor({ tasks, limit = 6 }) {
					this._tasks = [...tasks]
					this._limit = limit
					// 当前执行数
					this._count = 0
					// 任务数
					this._tasksCount = tasks.length
					// 已完成数
					this._finishedCount = 0
			}
			run() {
					return new Promise(resolve => {
							if (this._tasksCount === 0) {
									resolve()
									return
							}

							const { _tasks } = this
							const _run = function () {
									const idle = Math.min(_tasks.length, this._limit - this._count)
									for (let i = 0; i < idle; i++) {
											this._count++
											const task = _tasks.shift()
											task().finally(() => {
													this._count--
													this._finishedCount++
													if(this._finishedCount < this._tasksCount) {
															_run()
													} else {
															resolve()
													}
											})
									}
							}.bind(this)

							_run()
					})
			}
	}

	main()

})();