// ==UserScript==
// @name QQ Mail File Share
// @namespace undefined
// @version 0.0.3
// @description qq邮箱中转站文件分享,秒传原理
// @author https://github.com/cong99
// @match *://mail.qq.com/*
// @match *://cong99.gitee.io/qq_mail_file_share/*
// @require https://code.jquery.com/jquery-latest.js
// @require https://cdnjs.cloudflare.com/ajax/libs/bootpag/1.0.7/jquery.bootpag.min.js
// @run-at document-start
// @grant unsafeWindow
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// ==/UserScript==
(function() {
'use strict'
var ConstantValue = {
current_version: '0.0.3',
param_start_key: '#QQ_FILE_SHARE_',
param_start_action_key: '#QQ_FILE_ACTION_',
tip_page_host: 'cong99.gitee.io',
tip_page_pathname: '/qq_mail_file_share/',
tip_page_url: 'https://cong99.gitee.io/qq_mail_file_share/',
main_page_pathname: '/cgi-bin/frame_html',
login_page_pathname: '/cgi-bin/login',
script_page_url: 'https://greasyfork.org/zh-CN/scripts/392885',
share_link_url: 'https://mail.qq.com/',
login_info_api_url: 'https://mail.qq.com/cgi-bin/login?fun=psaread&rand=&delegate_url=&target=',
file_list_api_url: 'https://mail.qq.com/cgi-bin/ftnExs_files?t=ftn.json&s=list&ef=js&listtype=self&up=down&sorttype=createtime',
file_list_page_size: 10,
alert_param_error: '分享链接中所带参数错误',
alert_unlogin_error: '请登录qq邮箱之后再重新点击分享链接',
alert_get_file_list_error: '获取中转站文件列表失败',
alert_create_file_error: '创建分享文件失败',
tip_jump_error: '如果页面没有主动跳转, 点击这里=>',
tip_jump_update_error: '您的油猴脚本版本过低, 请及时更新=>'
}
var $ = $ || window.$
var encodeBase64 = btoa || window.btoa
var decodeBase64 = atob || window.atob
$(function(){
if (isTipPage()) {
var supportVerison = ($('#support-min-version').text() || '').trim()
if (isLowerVerison(ConstantValue.current_version, supportVerison)) {
var tipALink = `<a href="${ConstantValue.script_page_url}" target="_blank">脚本地址</a>`
var tipDom = $('#page-tip').html(ConstantValue.tip_jump_update_error + tipALink)
} else {
if (location.hash && location.hash.indexOf(ConstantValue.param_start_key) > -1) {
var qqShareHref = ConstantValue.share_link_url + location.hash
var tipALink = `<a href="${qqShareHref}" target="_blank">下载地址</a>`
var tipDom = $('#page-tip').html(ConstantValue.tip_jump_error + tipALink)
location.href = qqShareHref
}
}
} else {
if (isShareLinkFirstLoad()) {
getLoginRedirect()
} else if (isMainPage()) {
// console.log('shareParam', shareParam)
var shareParam = getShareParam()
var fileShareHelper = new FileShareHelper()
if (shareParam) {
// 秒传
var quickUrl = createQuickUploadUrl(shareParam)
request({url: quickUrl}).then(res => {
var responseText = res.response
var json = {}
try {
json = eval(responseText)
} catch(err) {
alert(ConstantValue.alert_create_file_error + ' 失败原因: ' + responseText)
}
if (Number(json.errcode) === 0) {
alert('获取到分享文件: ' + shareParam.sName)
}
}).catch(err => {
// 接口一定返回200, 这里写着以防万一而已
alert(ConstantValue.alert_create_file_error)
}).finally(() => {
// 设置hash为空
location.hash = ''
// 初始化table
fileShareHelper.init(true)
})
} else {
fileShareHelper.init()
}
}
}
})
// 原始分享链接 加载
function isShareLinkFirstLoad() {
var hash = (location.hash || '').trim()
if (hash && hash.startsWith(ConstantValue.param_start_key)) {
return true
} else {
return false
}
}
// 中途提供跳转的提示页
function isTipPage() {
var pathname = location.pathname
var checkPathName = ConstantValue.tip_page_pathname
return location.host === ConstantValue.tip_page_host && ( pathname === checkPathName || pathname === (checkPathName + 'index') || pathname === (checkPathName + 'index.html'))
}
function isMainPage(){
var regx = /sid=/g
return !!location.search.match(regx) && ConstantValue.main_page_pathname === location.pathname
}
function getSid() {
return urlArgs()['sid']
}
function urlArgs() {
var args = {}
var query = location.search.substring(1)
var pairs = query.split("&")
for (var i = 0; i < pairs.length; i++) {
var pos = pairs[i].indexOf("=")
if (pos == -1) {
continue
}
var name = pairs[i].substring(0, pos)
var value = pairs[i].substring(pos + 1)
args[name] = value
}
return args
}
function isLowerVerison(currentVersion, checkVersion) {
return versionToNum(currentVersion) < versionToNum(checkVersion)
}
function versionToNum(version) {
if (!version) {
return 0
}
var versionList = version.split('.')
if (versionList.length < 3) {
return 0
} else {
versionList = versionList.map(item => parseInt(item))
return versionList[0] * 100 * 100 + versionList[1] * 100 + versionList[2]
}
}
function getShareParam() {
var hash = (location.hash || '').trim()
if (hash && hash.startsWith(ConstantValue.param_start_action_key)) {
var paramStr = hash.substring(ConstantValue.param_start_action_key.length)
var paramJson = {}
try {
var paramJsonStr = decodeBase64(paramStr)
paramJson = JSON.parse(paramJsonStr)
} catch(err) {
alert(ConstantValue.alert_param_error)
return false
}
// 校验参数是否正确
if (paramJson.sName && paramJson.nSize && paramJson.sSHA) {
paramJson.sName = decodeURIComponent(paramJson.sName)
return paramJson
} else {
alert(ConstantValue.alert_param_error)
return false
}
} else {
return false
}
}
function getLoginRedirect() {
request({
url: ConstantValue.login_info_api_url
}).then(response => {
// console.log('response', response.finalUrl)
var finalUrl = response.finalUrl || ''
if (finalUrl && finalUrl.indexOf(ConstantValue.main_page_pathname) > -1) {
location.href = response.finalUrl + replaceStartKey(location.hash)
} else {
// 未登录 https://mail.qq.com/cgi-bin/login?fun=psaread&rand=&delegate_url=&target=
alert(ConstantValue.alert_unlogin_error)
}
})
}
function isJsonStr(str) {
if (typeof str == 'string') {
try {
JSON.parse(str)
return true
} catch(e) {
// console.log(e)
return false
}
} else {
return false
}
}
function replaceStartKey(hash) {
return hash.replace(ConstantValue.param_start_key, ConstantValue.param_start_action_key)
}
function request(option) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: option.method || 'GET',
url: option.url,
dataType: option.data_type || 'text',
headers: option.headers || {
},
onload: function (response) {
resolve(response)
},
onerror: function (response) {
reject(response)
}
})
})
}
function formatFileSize(fileSize) {
if (fileSize < 1024) {
return fileSize + 'B'
} else if (fileSize < (1024*1024)) {
var temp = fileSize / 1024
temp = temp.toFixed(2)
return temp + 'KB'
} else if (fileSize < (1024*1024*1024)) {
var temp = fileSize / (1024*1024)
temp = temp.toFixed(2)
return temp + 'MB'
} else {
var temp = fileSize / (1024*1024*1024)
temp = temp.toFixed(2)
return temp + 'GB'
}
}
function formatTime(timestamp) {
var date = new Date(timestamp * 1000)
var Y = date.getFullYear() + '-'
var M = (date.getMonth() + 1 < 10 ? '0' + (date.getMonth() + 1) : date.getMonth() + 1) + '-'
var D = date.getDate() + ' '
var h = date.getHours() + ':'
var m = date.getMinutes() + ':'
var s = date.getSeconds()
return Y + M + D + h + m + s
}
function formatExpireTime(second) {
if (second < 0) {
return '无限期'
} else if (second < 60) {
return second + '秒'
} else if (second < 60 * 60) {
return Math.ceil(second / 60) + '分钟'
} else if (second < 60 * 60 * 24) {
return Math.ceil(second / 60 / 60) + '小时'
} else {
return Math.ceil(second / 60 / 60 / 24) + '天'
}
}
function getDownloadUrl(sid, fileItem) {
return `https://mail.qq.com/cgi-bin/ftnDownload302?sid=${sid}&fid=${fileItem.sFileId}&code=${fileItem.sFetchCode}&k=${fileItem.sKey}`
}
function getShareLink(fileItem) {
var paramJson = {}
paramJson.sName = encodeURIComponent(fileItem.sName)
paramJson.nSize = fileItem.nSize
paramJson.sSHA = fileItem.sSHA
return ConstantValue.tip_page_url + ConstantValue.param_start_key + encodeBase64(JSON.stringify(paramJson))
}
function createQuickUploadUrl(fileItem) {
var sid = getSid()
return `https://mail.qq.com/cgi-bin/ftnCreatefile?uin=&ef=js&resp_charset=UTF8&s=ftnCreate&sid=${sid}&dirid=&path=${fileItem.sName}&size=${fileItem.nSize}&sha=${fileItem.sSHA}&sha3=&appid=2&loc=ftnCreatefile,ftnCreatefile,ftnCreate,ftn2`
}
function FileShareHelper(){
this.init = function(showTable) {
insertPurecss()
var listPageDiv = createFileListPage()
var switchBtn = createSwitchBtn(listPageDiv)
createToolDom()
if (showTable) {
switchBtn.click()
}
}
function insertPurecss() {
var myCssStr = `
.button-success,
.button-error,
.button-warning,
.button-secondary {
color: white!important;
border-radius: 4px;
text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);
}
.button-success {background: rgb(28, 184, 65)}
.button-error {background: rgb(202, 60, 60)}
.button-warning {background: rgb(223, 117, 20)}
.button-secondary {background: rgb(66, 184, 221)}
.pure-table th {overflow: hidden;text-overflow: ellipsis;white-space: nowrap}
.w80 {width: 80px; margin: auto;}
.w120 {width: 120px; margin: auto;}
.w160 {width: 160px; margin: auto;}
.w240 {width: 240px; margin: auto;}
.w360 {width: 3600px; margin: auto;}
.bootpag li {display: inline-block;}
.bootpag a {display: inline-block; height: 35px; width: 35px; background: #FFF; line-height: 35px; margin: 0 5px; font-weight: bold;}
.bootpag .active a {
background: rgb(202, 60, 60);
color: #Fff;
}
.modal-mask {
position: fixed;
top: 0;
left: 0;
height: 100%;
width: 100%;
z-index: 100;
background: #333;
opacity: 0.5;
}
.modal-dialog {
position: fixed;
top: 25%;
left: 33%;
z-index: 101;
height: 130px;
width: 500px;
background: #fff;
border: 1px solid rgba(0,0,0,.2);
border-radius: 6px;
padding: 20px;
}
.modal-dialog-title {
font-size: 16px;
line-height: 16px;
font-weight: bold;
width: 460px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.modal-dialog-content {
height: 80px;
line-height: 14px;
font-size: 14px;
margin: 10px 0;
max-width: 100%;
word-break: break-all;
overflow-y: auto;
}
.modal-dialog-tip {
color: rgb(28, 184, 65);
}
.modal-dialog-close-btn {
float: right;
}
`
var $pureCss = $(`<link rel="stylesheet" href="https://unpkg.com/[email protected]/build/pure-min.css">`)
var $myCss = $(`<style type="text/css">${myCssStr}</style>`)
$('head').append($pureCss)
$('head').append($myCss)
}
function createToolDom() {
var $modalMask = $(`<div id="modal-mask" class="modal-mask"></div>`)
var $modalDialog = $(`<div id="modal-dialog" class="modal-dialog">
<h3 id="modal-dialog-title" class="modal-dialog-title">title</h3>
<div id="modal-dialog-content" class="modal-dialog-content">content</div>
<span class="modal-dialog-tip">已将链接复制到剪贴板!</span>
<button id="modal-dialog-close-btn" class="pure-button button-error modal-dialog-close-btn">关闭</button></div>`)
$('body').append($modalMask)
$('body').append($modalDialog)
$('#modal-mask').hide()
$('#modal-dialog').hide()
$('#modal-dialog-close-btn').click(function() {
$('#modal-mask').hide()
$('#modal-dialog').hide()
})
}
function openDialog(title, content) {
$('#modal-dialog-title').html(title)
$('#modal-dialog-title').attr('title', title)
$('#modal-dialog-content').html(content)
$('#modal-mask').show()
$('#modal-dialog').show()
}
function tipInfo(content, type, duration) {
setTimeout(() => {
// close
}, duration || 1000)
}
function createSwitchBtn(listPageDiv) {
var switchToList = '切换至分享列表'
var switchToOrigin = '切换至原页面'
var $switchBtn = $(`<button class="pure-button button-error" style="position: fixed;top: 2px;right: 200px;z-index: 99">${switchToList}</button>`)
$switchBtn.click(function() {
if ($switchBtn.text() === switchToList) {
listPageDiv.show()
$switchBtn.text(switchToOrigin)
} else {
listPageDiv.hide()
$switchBtn.text(switchToList)
}
})
$('body').append($switchBtn)
return $switchBtn
}
function createFileListPage() {
var $pageDiv = $('<div style="position: fixed;height: 100%;width: 100%;z-index: 98;top: 0;padding-top: 50px; background: #333"></div>')
$pageDiv.hide()
var $contentWraperDiv = $('<div style="width: 80%; margin: auto;"></div>')
var $refreashBtn = $(`<button class="pure-button button-secondary">刷新[上传过文件之后建议刷新]</button>`)
var $emptyDiv = $(`<div style="text-align: center; color:#fff;"><h1>中转站暂无数据...</h1></div>`)
$emptyDiv.hide()
var $paginationDiv = $(`<div style="text-align: center; margin-top: 25px;"></div>`)
var $fileTable = $(`<table class="pure-table pure-table-horizontal" style="width: 100%; margin: 10px auto; background: #fff"></table>`)
var $tableHead = $(`<thead style="text-align: center"><tr><th>文件名</th><th>文件大小</th><th>上传时间</th><th>过期时间</th><th>下载次数</th><th>操作</th></tr></thead>`)
var $tableBody = $(`<tbody></tbody>`)
$refreashBtn.click(function () {
// 这边有问题, 分页组件要注销然后再重建---->当前没有好办法注销,所以隐藏该按钮
// firstLoadData($paginationDiv, $emptyDiv, $tableBody)
})
$fileTable.append($tableHead)
$fileTable.append($tableBody)
// $contentWraperDiv.append($refreashBtn)
$contentWraperDiv.append($fileTable)
$contentWraperDiv.append($emptyDiv)
$contentWraperDiv.append($paginationDiv)
$pageDiv.append($contentWraperDiv)
$('body').append($pageDiv)
firstLoadData($paginationDiv, $emptyDiv, $tableBody)
return $pageDiv
}
function insertFileListIntoTable(tableBodyDom, fileList) {
var sid = getSid()
var fileTrList = fileList.map(fileItem => {
return `<tr><th><div class="w120" title="${fileItem.sName}">${fileItem.sName}</div></th>
<th><div class="w80">${formatFileSize(fileItem.nSize)}</div></th>
<th><div class="w160">${formatTime(fileItem.nCreateTime)}</div></th>
<th><div class="w80">${formatExpireTime(fileItem.nExpireTime)}</div></th>
<th><div class="w120">${fileItem.nDownCnt}</div></th>
<th><div class="w240">
<button class="button-warning pure-button share-link-btn" filename="${fileItem.sName}" link="${getShareLink(fileItem)}">分享链接</button>
<a class="button-success pure-button" href="${getDownloadUrl(sid, fileItem)}" target="_blank">下载</a>
</div></th>
</tr>`
})
tableBodyDom.html('')
tableBodyDom.append($(fileTrList.join('')))
$('.share-link-btn').click(function() {
var title = $(this).attr('filename') + ' 分享地址:'
var content = $(this).attr('link')
GM_setClipboard(content, 'text')
openDialog(title, content)
})
}
function firstLoadData(paginationDivDom, emptyDivDom, tableBodyDom) {
// 初始获取第一页,创建分页组件
window.currentPageNum = 1
requestFileList(window.currentPageNum, tableBodyDom).then(listRes => {
// 处理dom
var total = listRes.nTotal || 0
total = parseInt(total)
if (total) {
createPagination(paginationDivDom, total, tableBodyDom)
} else {
paginationDivDom.hide()
emptyDivDom.show()
}
})
}
function createPagination(paginationDivDom, total, tableBodyDom) {
var pageSize = ConstantValue.file_list_page_size
var totalPage = total % pageSize === 0 ? total / pageSize : Math.ceil(total / pageSize)
paginationDivDom.bootpag({
total: totalPage,
page: 1,
maxVisible: 5,
leaps: true,
firstLastUse: true
}).on('page', function(event, num){
window.currentPageNum = num
requestFileList(num, tableBodyDom)
})
}
function requestFileList(page, tableBodyDom) {
var sid = getSid()
// 接口页码是以0开始的
return request({
url: ConstantValue.file_list_api_url + `&sid=${sid}&page=${page - 1}&pagesize=${ConstantValue.file_list_page_size}`,
data_type: 'json'
}).then(response => {
var responseText = response.responseText
var json = {}
try {
json = eval(responseText)
} catch(err) {
alert(ConstantValue.alert_get_file_list_error)
}
insertFileListIntoTable(tableBodyDom, json.oFiles)
// console.log(json)
return json
})
}
}
})()