1// ==UserScript== 2// @name Video Together ä¸èµ·çè§é¢ 3// @namespace https://videotogether.github.io/ 4// @version 1760266471 5// @description Watch video together ä¸èµ·çè§é¢ 6// @author [email protected] 7// @match *://*/* 8// @icon https://videotogether.github.io/icon/favicon-32x32.png 9// @grant GM.xmlHttpRequest 10// @grant GM_addElement 11// @grant GM.setValue 12// @grant GM.getValue 13// @grant GM.getTab 14// @grant GM.saveTab 15// @connect 2gether.video 16// @connect api.2gether.video 17// @connect api.begin0114.wiki 18// @connect api.panghair.com 19// @connect vt.panghair.com 20// @connect raw.githubusercontent.com 21// @connect fastly.jsdelivr.net 22// @connect videotogether.oss-cn-hangzhou.aliyuncs.com 23// ==/UserScript== 24 25(async function () { 26 if (['challenges.cloudflare.com'].indexOf(window.location.hostname) != -1) { 27 return; 28 } 29 let isDevelopment = false; 30 31 if (document instanceof XMLDocument) { 32 return; 33 } 34 35 let version = '9999999999' 36 // 1760266471 37 let type = 'website' 38 function getBrowser() { 39 switch (type) { 40 case 'Safari': 41 return browser; 42 case 'Chrome': 43 case 'Firefox': 44 return chrome; 45 } 46 } 47 let isExtension = (type == "Chrome" || type == "Safari" || type == "Firefox"); 48 let isWebsite = (type == "website" || type == "website_debug"); 49 let isUserscript = (type == "userscript"); 50 let websiteGM = {}; 51 let extensionGM = {}; 52 const encodedChinaCdnA = 'aHR0cHM6Ly92aWRlb3RvZ2V0aGVyLm9zcy1jbi1oYW5nemhvdS5hbGl5dW5jcy5jb20=' 53 const encodeFastlyJsdelivrCdn = 'aHR0cHM6Ly9mYXN0bHkuanNkZWxpdnIubmV0L2doL1ZpZGVvVG9nZXRoZXIvVmlkZW9Ub2dldGhlckBsYXRlc3Q=' 54 function getCdnPath(encodedCdn, path) { 55 const cdn = encodedCdn.startsWith('https') ? encodedCdn : atob(encodedCdn); 56 return `${cdn}/${path}`; 57 } 58 async function getCdnConfig(encodedCdn) { 59 return fetch(getCdnPath(encodedCdn, 'release/cdn-config.json')).then(r => r.json()) 60 } 61 async function getChinaCdnB() { 62 return getCdnConfig(encodedChinaCdnA).then(c => c.jsCdnHostChina) 63 } 64 65 function getGM() { 66 if (type == "website" || type == "website_debug") { 67 return websiteGM; 68 } 69 if (type == "Chrome" || type == "Safari" || type == "Firefox") { 70 return extensionGM; 71 } 72 return GM; 73 } 74 75 function getRealTableName(table) { 76 return table.replace('-mini', ''); 77 } 78 79 setInterval(() => { 80 if (isWebsite) { 81 (function () { 82 const iframes = document.getElementsByTagName('iframe'); 83 for (const iframe of iframes) { 84 try { 85 if (iframe.contentWindow.VideoTogetherParentInject != true && 86 window.location.origin === iframe.contentWindow.location.origin) { 87 console.log("inject to iframe"); 88 const script = document.createElement('script'); 89 script.src = '/play/vt.zh-cn.website.js?timestamp=' + version; 90 iframe.contentWindow.document.body.appendChild(script); 91 iframe.contentWindow.VideoTogetherParentInject = true; 92 } 93 } catch (error) { 94 } 95 } 96 })(); 97 } 98 }, 2000); 99 100 if (type == "website" || type == "website_debug") { 101 102 getGM().setValue = async (key, value) => { 103 return localStorage.setItem(key, JSON.stringify(value)); 104 } 105 106 getGM().getValue = async (key) => { 107 return JSON.parse(localStorage.getItem(key)); 108 } 109 110 getGM().getTab = async () => { 111 let tab = sessionStorage.getItem('VideoTogetherTab'); 112 return tab == null ? {} : JSON.parse(tab); 113 } 114 115 getGM().saveTab = async (tab) => { 116 return sessionStorage.setItem('VideoTogetherTab', JSON.stringify(tab)); 117 } 118 119 getGM().xmlHttpRequest = async (props) => { 120 try { 121 fetch(props.url, { 122 method: props.method, 123 body: props.method == "GET" ? undefined : JSON.stringify(props.data) 124 }) 125 .then(r => r.text()) 126 .then(text => props.onload({ responseText: text })) 127 .catch(e =>
127 props.onerror(e)); 128 } catch (e) { 129 props.onerror(e); 130 } 131 } 132 } 133 if (type == "Chrome" || type == "Safari" || type == "Firefox") { 134 getGM().setValue = async (key, value) => { 135 return await new Promise((resolve, reject) => { 136 try { 137 let item = {}; 138 item[key] = value; 139 getBrowser().storage.local.set(item, function () { 140 resolve(); 141 }); 142 } catch (e) { 143 reject(e); 144 } 145 }) 146 } 147 getGM().getValue = async (key) => { 148 return await new Promise((resolve, reject) => { 149 try { 150 getBrowser().storage.local.get([key], function (result) { 151 resolve(result[key]); 152 }); 153 } catch (e) { 154 reject(e); 155 } 156 157 }) 158 } 159 getGM().getTab = async () => { 160 return await new Promise((resolve, reject) => { 161 try { 162 getBrowser().runtime.sendMessage(JSON.stringify({ type: 1 }), function (response) { 163 resolve(response); 164 }) 165 } catch (e) { 166 reject(e); 167 } 168 169 }) 170 } 171 getGM().saveTab = async (tab) => { 172 return await new Promise((resolve, reject) => { 173 try { 174 getBrowser().runtime.sendMessage(JSON.stringify({ type: 2, tab: tab }), function (response) { 175 resolve(response); 176 }) 177 } catch (e) { 178 reject(e); 179 } 180 }) 181 } 182 getGM().xmlHttpRequest = async (props) => { 183 try { 184 getBrowser().runtime.sendMessage(JSON.stringify({ type: 3, props: props }), function (response) { 185 if (response.error != undefined) { 186 throw response.error; 187 } 188 props.onload(response); 189 }) 190 } catch (e) { 191 props.onerror(e); 192 } 193 } 194 } 195 196 if (isExtension) { 197 let vtEnabled = await getGM().getValue('vtEnabled'); 198 if (vtEnabled === false) { 199 getBrowser().runtime.sendMessage(JSON.stringify({ type: 4, enabled: false })); 200 return; 201 } else { 202 getBrowser().runtime.sendMessage(JSON.stringify({ type: 4, enabled: true })); 203 } 204 } 205 206 207 const languages = ['en-us', 'zh-cn', 'ja-jp']; 208 let language = 'en-us'; 209 let settingLanguage = undefined; 210 try { 211 settingLanguage = await getGM().getValue("DisplayLanguage"); 212 } catch (e) { }; 213 214 if (typeof settingLanguage != 'string') { 215 settingLanguage = navigator.language; 216 } 217 if (typeof settingLanguage == 'string') { 218 settingLanguage = settingLanguage.toLowerCase(); 219 if (languages.includes(settingLanguage)) { 220 language = settingLanguage; 221 } else { 222 const settingLanguagePrefix = settingLanguage.split('-')[0]; 223 for (let i = 0; i < languages.length; i++) { 224 const languagePrefix = languages[i].split('-')[0]; 225 if (settingLanguagePrefix === languagePrefix) { 226 language = languages[i]; 227 break; 228 } 229 } 230 } 231 } 232 233 let vtRefreshVersion = version + language; 234 try { 235 let publicVtVersion = await getGM().getValue("PublicVtVersion") 236 if (publicVtVersion != null) { 237 vtRefreshVersion = vtRefreshVersion + String(publicVtVersion); 238 } 239 } catch (e) { }; 240 console.log(vtRefreshVersion) 241 242 let cachedVt = null; 243 /* 244 try { 245 let vtType = isWebsite ? "website" : "user"; 246 let privateCachedVt = await getGM().getValue("PrivateCachedVt"); 247 let cachedVersion = null; 248 try { 249 cachedVersion = privateCachedVt['version']; 250 } catch { }; 251 if (cachedVersion == vtRefreshVersion) { 252 cachedVt = privateCachedVt['data']; 253 } else { 254 console.log("Refresh VT"); 255 fetch(getCdnPath(encodeFastlyJsdelivrCdn, `release/vt.${language}.${vtType}.js?vtRefreshVersion=${vtRefreshVersion}`)) 256 .then(r => r.text()) 257 .then(data => getGM().setValue('PrivateCachedVt', { 258 'version': vtRefreshVersion, 259 'data': data 260 })) 261 .catch(() => { 262 getChinaCdnB().then(chinaCdnB => fetch(getCdnPath(chinaCdnB, `release/vt.${language}.${vtType}.js?vtRefreshVersion=${vtRefreshVersion}`))) 263 .then(r => r.text()) 264 .then(data => getGM().setValue('PrivateCachedVt', { 265 'version': vtRefreshVersion, 266 'data': data 267 })) 268 }) 269 } 270 } catch (e) { }; 271 */ 272 273 async function AppendKey(key) { 274 let keysStr = await getGM().getValue("VideoTogetherKeys"); 275 let keys = new Set(JSON.parse(keysStr)); 276 keys.add(key); 277 await getGM().setValue("VideoTogetherKeys", JSON.stringify(Array.from(keys))); 278 } 279 280 async function GetKeys() { 281 let keysStr = await getGM().getValue("VideoTogetherKeys"); 282 try { 283 let keys = new Set(JSON.parse(keysStr)); 284 return Array.from(keys); 285 } catch (e) { 286 await getGM().setValue("VideoTogetherKeys", "[]"); 287 return []; 288 } 289 } 290 291 function InsertInlineScript(content) { 292 try { 293 let inlineScript = document.createElement("script"); 294 inlineScript.textContent = content; 295 document.head.appendChild(inlineScript); 296 } catch { } 297 try { 298 if (isUserscript) { 299 GM_addElement('script', { 300 textContent: content, 301 type: 'text/javascript' 302 }); 303 } 304 } catch { } 305 try { 306 if (isWebsite) { 307 eval(content); 308 } 309 } catch { } 310 } 311 312 function InsertInlineJs(url) { 313 try { 314 getGM().xmlHttpRequest({ 315 method: "GET", 316 url: url, 317 onload: function (response) { 318 InsertInlineScript(response.responseText); 319 } 320 }) 321 } catch (e) { }; 322 } 323 324 async function SetTabStorage(data) { 325 try { 326 let tabObj = await getGM().getTab(); 327 tabObj.VideoTogetherTabStorage = data; 328 await getGM().saveTab(tabObj); 329 window.postMessage({ 330 source: "VideoTogether", 331 type: 19, 332 data: tabObj.VideoTogetherTabStorage 333 }) 334 } catch (e) { }; 335 } 336 337 if (window.VideoTogetherLoading) { 338 return; 339 } 340 window.VideoTogetherLoading = true; 341 let ExtensionInitSuccess = false;
342 343 let isTrustPageCache = undefined; 344 function isTrustPage() { 345 if (isDevelopment) { 346 return true; 347 } 348 if (window.location.protocol != 'https:') { 349 return false 350 } 351 352 if (isTrustPageCache == undefined) { 353 const domains = [ 354 '2gether.video', 'videotogether.github.io' 355 ]; 356 357 const hostname = window.location.hostname; 358 isTrustPageCache = domains.some(domain => hostname === domain || hostname.endsWith(`.${domain}`)); 359 } 360 return isTrustPageCache; 361 } 362 const indexedDbWriteHistory = {} 363 function needTrustPage() { 364 if (!isTrustPage()) { 365 throw "not trust page" 366 } 367 } 368 369 window.addEventListener("message", async e => { 370 if (e.data.source == "VideoTogether") { 371 switch (e.data.type) { 372 case 13: { 373 let url = new URL(e.data.data.url); 374 if (!url.hostname.endsWith("2gether.video") 375 && !url.hostname.endsWith("xn--6kr25xemln66b.com") 376 && !url.hostname.endsWith("panghair.com") 377 && !url.hostname.endsWith("videotogether.github.io") 378 && !url.hostname.endsWith("aliyuncs.com")) { 379 console.error("permission error", e.data); 380 return; 381 } 382 getGM().xmlHttpRequest({ 383 method: e.data.data.method, 384 url: e.data.data.url, 385 data: e.data.data.data, 386 onload: function (response) { 387 let data = null; 388 try { 389 data = JSON.parse(response.responseText); 390 } catch (e) { }; 391 window.postMessage({ 392 source: "VideoTogether", 393 type: 14, 394 data: { 395 id: e.data.data.id, 396 data: data, 397 text: response.responseText 398 } 399 }) 400 }, 401 onerror: function (error) { 402 window.postMessage({ 403 source: "VideoTogether", 404 type: 14, 405 data: { 406 id: e.data.data.id, 407 error: error, 408 } 409 }) 410 } 411 }) 412 break; 413 } 414 case 15: { 415 if (window.location.hostname.endsWith("videotogether.github.io") 416 || window.location.hostname.endsWith("2gether.video") 417 || e.data.data.key.startsWith("Public") 418 || isWebsite 419 || isDevelopment) { 420 getGM().setValue(e.data.data.key, e.data.data.value) 421 AppendKey(e.data.data.key); 422 break; 423 } else { 424 console.error("permission error", e.data); 425 } 426 break; 427 } 428 case 17: { 429 ExtensionInitSuccess = true; 430 break; 431 } 432 case 18: { 433 await SetTabStorage(e.data.data); 434 break; 435 } 436 case 2001: { 437 getBrowser().runtime.sendMessage(JSON.stringify(e.data), response => { 438 const realTableName = getRealTableName(e.data.data.table) 439 if (indexedDbWriteHistory[realTableName] == undefined) { 440 indexedDbWriteHistory[realTableName] = {}; 441 } 442 indexedDbWriteHistory[realTableName][e.data.data.key] = true; 443 window.postMessage({ 444 source: "VideoTogether", 445 type: 2003, 446 data: { 447 id: e.data.data.id, 448 table: e.data.data.table, 449 key: e.data.data.key, 450 error: response.error 451 } 452 }) 453 }) 454 break; 455 } 456 case 2002: { 457 try { 458 const realTableName = getRealTableName(e.data.data.table) 459 if (!indexedDbWriteHistory[realTableName][e.data.data.key]) { 460 needTrustPage(); 461 } 462 } catch { 463 needTrustPage(); 464 } 465 getBrowser().runtime.sendMessage(JSON.stringify(e.data), response => { 466 window.postMessage({ 467 source: "VideoTogether", 468 type: 2004, 469 data: { 470 id: e.data.data.id, 471 table: e.data.data.table, 472 key: e.data.data.key, 473 data: response.data, 474 error: response.error 475 } 476 }) 477 }) 478 break; 479 } 480 case 2005: { 481 needTrustPage(); 482 getBrowser().runtime.sendMessage(JSON.stringify(e.data), response => { 483 window.postMessage({ 484 source: "VideoTogether", 485 type: 2006, 486 data: { 487 id: e.data.data.id, 488 table: e.data.data.table, 489 regex: e.data.data.regex, 490 data: response.data, 491 error: response.error 492 } 493 }) 494 }) 495 break; 496 } 497 case 2007: { 498 needTrustPage(); 499 getBrowser().runtime.sendMessage(JSON.stringify(e.data), response => { 500 window.postMessage({ 501 source: "VideoTogether", 502 type: 2008, 503 data: { 504 id: e.data.data.id, 505 table: e.data.data.table, 506 key: e.data.data.key, 507 error: response.error 508 } 509 }) 510 }) 511 break; 512 } 513 case 2009: { 514 getBrowser().runtime.sendMessage(JSON.stringify(e.data), response => { 515 window.postMessage({ 516 source: "VideoTogether", 517 type: 2010, 518 data: { 519 data: JSON.parse(response) 520 } 521 }) 522 }) 523 break; 524 } 525 case 3009: { 526 getBrowser().runtime.sendMessage(JSON.stringify(e.data)) 527 break; 528 } 529 case 3010: { 530 needTrustPage(); 531 getBrowser().runtime.sendMessage(JSON.stringify(e.data), response => { 532 window.postMessage({ 533 source: "VideoTogether", 534 type: 3011, 535 data: { 536 id: e.data.data.id, 537 error: response.error 538 } 539 }) 540 }) 541 break; 542 } 543 } 544 } 545 }); 546 547 let isMain = window.self == window.top; 548 549 async function PostStorage() { 550 try { 551 let keys = await GetKeys(); 552 let data = {} 553 for (let i = 0; i < keys.length; i++) { 554 data[keys[i]] = await getGM().getValue(keys[i]); 555 if (data[keys[i]] == 'true') { 556 data[keys[i]] = true; 557 } 558 if (data[keys[i]] == 'false') { 559 data[keys[i]] = false;
560 } 561 } 562 data["UserscriptType"] = type; 563 data["LoaddingVersion"] = version; 564 data["VideoTogetherTabStorageEnabled"] = true; 565 data['ExtensionLanguages'] = languages; 566 try { 567 data["VideoTogetherTabStorage"] = (await getGM().getTab()).VideoTogetherTabStorage; 568 } catch (e) { 569 data["VideoTogetherTabStorageEnabled"] = false; 570 } 571 window.postMessage({ 572 source: "VideoTogether", 573 type: 16, 574 data: data 575 }, "*"); 576 } catch { } 577 } 578 579 PostStorage(); 580 setInterval(() => { 581 PostStorage(); 582 }, 1000); 583 584 function insertJs(url) { 585 let script = document.createElement('script'); 586 script.type = 'text/javascript'; 587 script.src = url; 588 (document.body || document.documentElement).appendChild(script); 589 } 590 591 let wrapper = document.createElement("div"); 592 wrapper.innerHTML = `<div id="videoTogetherLoading" style="display:none;"></div>`; 593 594 (document.body || document.documentElement).appendChild(wrapper); 595 let script = document.createElement('script'); 596 script.type = 'text/javascript'; 597 switch (type) { 598 case "userscript": 599 script.src = getCdnPath(encodeFastlyJsdelivrCdn, `release/vt.${language}.user.js?timestamp=${version}`); 600 break; 601 case "Chrome": 602 case "Safari": 603 case "Firefox": 604 let inlineDisabled = false; 605 let evalDisabled = false; 606 let urlDisabled = false; 607 let hotUpdated = false; 608 document.addEventListener("securitypolicyviolation", (e) => { 609 if (hotUpdated) { 610 return; 611 } 612 for (let blockedStr of [e.blockedURI, e.sample]) { 613 for (let extensionUrl of ["2gether.video", "jsdelivr.net"]) { 614 try { 615 if (blockedStr.indexOf(extensionUrl) != -1) { 616 urlDisabled = true; 617 } 618 } catch { } 619 620 } 621 } 622 623 if (urlDisabled) { 624 console.log("hot update is not successful") 625 insertJs(getBrowser().runtime.getURL(`vt.${language}.user.js`)); 626 hotUpdated = true; 627 } 628 }); 629 if (isDevelopment) { 630 script.src = getBrowser().runtime.getURL(`vt.${language}.user.js`); 631 } else { 632 script.src = getBrowser().runtime.getURL(`load.${language}.js`); 633 } 634 script.setAttribute("cachedVt", cachedVt); 635 break; 636 case "userscript_debug": 637 script.src = `http://127.0.0.1:7000/release/vt.debug.${language}.user.js?timestamp=` + parseInt(Date.now()); 638 break; 639 case "website": 640 script.src = '/play/vt.zh-cn.website.js?timestamp=' + version; 641 break; 642 case "website_debug": 643 script.src = `http://127.0.0.1:7000/release/vt.debug.${language}.website.js?timestamp=` + parseInt(Date.now()); 644 break; 645 } 646 647 if (isWebsite || isUserscript) { 648 /* 649 if (cachedVt != null) { 650 InsertInlineScript(cachedVt); 651 } 652 */ 653 setTimeout(() => { 654 if (!ExtensionInitSuccess) { 655 (document.body || document.documentElement).appendChild(script); 656 if (isWebsite) { 657 // keep this inline inject because shark browser needs this 658 InsertInlineJs(script.src); 659 } 660 try { 661 GM_addElement('script', { 662 src: script.src, 663 type: 'text/javascript' 664 }) 665 } catch { } 666 } 667 }, 10); 668 } else { 669 (document.body || document.documentElement).appendChild(script); 670 } 671 672 // fallback to china service 673 setTimeout(async () => { 674 try { 675 document.querySelector("#videoTogetherLoading").remove() 676 } catch { }
677 if (type == "Chrome" || type == "Firefox" || type == "Safari") { 678 return; 679 } 680 if (!ExtensionInitSuccess) { 681 let script = document.createElement('script'); 682 script.type = 'text/javascript'; 683 const chinaCdnB = await getChinaCdnB(); 684 script.src = getCdnPath(chinaCdnB, `release/vt.${language}.user.js`); 685 (document.body || document.documentElement).appendChild(script); 686 try { 687 if (isWebsite) { 688 InsertInlineJs(script.src); 689 } 690 691 GM_addElement('script', { 692 src: script.src, 693 type: 'text/javascript' 694 }) 695 } catch (e) { }; 696 } 697 }, 5000); 698 function filter(e) { 699 let target = e.target; 700 701 if (target.id != "videoTogetherLoading") { 702 return; 703 } 704 705 target.moving = true; 706 707 if (e.clientX) { 708 target.oldX = e.clientX; 709 target.oldY = e.clientY; 710 } else { 711 target.oldX = e.touches[0].clientX; 712 target.oldY = e.touches[0].clientY; 713 } 714 715 target.oldLeft = window.getComputedStyle(target).getPropertyValue('left').split('px')[0] * 1; 716 target.oldTop = window.getComputedStyle(target).getPropertyValue('top').split('px')[0] * 1; 717 718 document.onmousemove = dr; 719 document.ontouchmove = dr; 720 721 function dr(event) { 722 if (!target.moving) { 723 return; 724 } 725 if (event.clientX) { 726 target.distX = event.clientX - target.oldX; 727 target.distY = event.clientY - target.oldY; 728 } else { 729 target.distX = event.touches[0].clientX - target.oldX; 730 target.distY = event.touches[0].clientY - target.oldY; 731 } 732 733 target.style.left = Math.min(document.documentElement.clientWidth - target.clientWidth, Math.max(0, target.oldLeft + target.distX)) + "px"; 734 target.style.top = Math.min(document.documentElement.clientHeight - target.clientHeight, Math.max(0, target.oldTop + target.distY)) + "px"; 735 } 736 737 function endDrag() { 738 target.moving = false; 739 } 740 target.onmouseup = endDrag; 741 target.ontouchend = endDrag; 742 } 743 document.onmousedown = filter; 744 document.ontouchstart = filter; 745})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.