您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
avoid bots + more features
// ==UserScript== // @name ThundrPlus // @namespace http://tampermonkey.net/ // @version 2025-04-27 // @description avoid bots + more features // @author YassinMi // @match https://thundr.com/text // @icon https://www.google.com/s2/favicons?sz=64&domain=thundr.com // @grant none // @license MIT // ==/UserScript== //@ts-check /** * @typedef {{ip:string,id:string,country:string,sex:string}} PartnerInfo */ /** * @typedef {{partnerInfo:PartnerInfo,matchedAt:Date,matchId:string}} Match */ /** * @typedef {{disconnectedAt:Date,matchId:string, from:string?, to:string}} DisconnectedArgs when disconection is pushed form me the from are nulll */ /** * @typedef {{from:"You"|"Stranger",content:string}[]} ConversationContent */ /** * @typedef {{match:Match,conversationContent:ConversationContent,disconnectedBy:"You"|"Stranger",disconnectedAt:Date}} Interaction */ class DBLayer { /** * */ static verifyReadyForNewMatchEntry() { if (this.matchesBuffer.length) { var latestMatchEntry = this.matchesBuffer[this.matchesBuffer.length - 1] if (latestMatchEntry.disconnectArgs === undefined) { console.warn("not ready for new match, the last match isn't properly disconnected, an implicit closure is made") latestMatchEntry.disconnectArgs = { disconnectedAt: new Date(), matchId: latestMatchEntry.match.matchId, from: "", to: "" } } } } /** * * @param {string} matchId * @returns {{match:Match,matchRaw:any,disconnectArgs:DisconnectedArgs?,scrapedConvo:ConversationContent?}|undefined} */ static getMatchFromMatchBuffer(matchId) { return DBLayer.matchesBuffer.find(i => i.match.matchId === matchId); } /** * @type {DBLayer} */ static DB = new DBLayer() /** * @type {{match:Match,matchRaw:any,disconnectArgs:DisconnectedArgs?,scrapedConvo:ConversationContent?}[]} */ static matchesBuffer = [] /** * @type {string?} */ static latestKnownMatchId constructor() { } /** * * @param {string} matchId * @param {ConversationContent} scrapedConvo */ updateScrapedConvoForMatchId(matchId, scrapedConvo) { console.log(`DB Cache: updating match entry ${matchId} with scraped convo ${scrapedConvo?.length}`) var matchEntry = DBLayer.getMatchFromMatchBuffer(matchId) if (matchEntry === undefined) { console.log("no such match entry registred") throw new Error("no such match entry registred") } else { } var maybeExists = matchEntry.scrapedConvo if (maybeExists) { if ((!scrapedConvo) || (maybeExists.length > scrapedConvo.length)) { return; } else { } } matchEntry.scrapedConvo = scrapedConvo } getInteractionssByIP(ip) { var oldData = window.localStorage.getItem("convosOfIp_" + ip) var parsedOldData = JSON.parse(oldData ?? "[]") return parsedOldData; } /** * * @param {Interaction} interaction */ addInteraction(interaction) { console.log("DB: saving interaction:", interaction) var oldData = window.localStorage.getItem("convosOfIp_" + interaction.match.partnerInfo.ip) var parsedOldData = JSON.parse(oldData ?? "[]") parsedOldData.push(interaction) window.localStorage.setItem("convosOfIp_" + interaction.match.partnerInfo.ip, JSON.stringify(parsedOldData)) } /** * * @param {string} ip * @param {string} note */ storeIPNote(ip, note) { console.log("DB: saving note:", note, ip) window.localStorage.setItem("noteOfIp_" + ip, note) } /** * * @param {string} ip */ getIPNote(ip) { return window.localStorage.getItem("noteOfIp_" + ip) } } /** * this layer requires maintainance, it injects code that intercepts events and fires them in the the standard * TP_MATCHED, TP_MESSAGE, TP_DISCONNECTED, TP_INIT layer */ function hookEvents() { console.log("####### hookEvents called") if (window["tp_hookEvents_called"]) { throw new Error("hook events already called") } window["tp_hookEvents_called"] = true const originalLog = console.log; var matchLogDetector = function (...args) { if (args.length >= 2 && typeof args[0] === "string" && args[0].includes("$$$ matched! $$$")) { var arg = args[1] if (typeof arg === "object" && arg !== null && "match_id" in arg && "room" in arg && "partner" in arg) { return arg } } } var disconnectedLogDetector = function (...args) { if (args.length >= 2 && typeof args[0] === "string" && args[0].includes("$$$ user disconnected $$$")) { var arg = args[1] if (typeof arg === "object" && arg !== null && "match_id" in arg && "from" in arg && "to" in arg) { return arg } } return undefined } var disconnectedPushedFromMeLogDetector = function (...args) { if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("$$$ push disconnect $$$")) { var concat = args.join(""); var partnerId = (/**@type {string}*/(concat)).replace("$$$ push disconnect $$$", "").trim() return partnerId } return undefined } var experimentalPreDisconnectLog = function (...args) { if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("fired event start_text_m")) { return true } return false } var experimentalLefRoomLog = function (...args) { if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("left room")) { return true } return false } var startMatchLogDetector = function (...args) { if (args.length >= 1 && typeof args[0] === "string" && args[0].includes("$$$ start match $$$")) { return true } return false } // //$$$ push disconnect $$$ 230d75c2-d3f8-4bbb-86c0-16462add3fe7 console.log = function (...args) { originalLog("fake log") originalLog.apply(console, args); var maybeMatchLogArg = matchLogDetector(...args) var maybeDisconnectLogArg = disconnectedLogDetector(...args) var maybeDisconnectFromMeLogArg = disconnectedPushedFromMeLogDetector(...args) var experimentalPreDisconnectLog_ = experimentalPreDisconnectLog(...args) var experimentalLefRoomLog_ = experimentalLefRoomLog(...args) var startMatchLogDetector_ = startMatchLogDetector(...args) if (startMatchLogDetector_) { window.postMessage({ type: "TP_INIT", data: null }, "*"); } else if (maybeMatchLogArg) { console.log(" log detected as maybeMatchLogArg") var arg = maybeMatchLogArg; const summary = { matchId: arg.match_id, matchedAt: new Date(), /**@type {PartnerInfo} */ partnerInfo: { ip: arg.partner.ip, id: arg.partner.id, country: arg.partner?.country, sex: arg.partner.settings.profile.sex }, }; console.log("####### TP_MATCHED fired", summary) window.postMessage({ type: "TP_MATCHED", data: summary }, "*"); } else if (maybeDisconnectLogArg) { console.log(" log detected as maybeDisconnectLogArg") pollUpdateScrapedConvoForLatestMatch() var arg = maybeDisconnectLogArg; /** * @type {DisconnectedArgs} */ const summary = { matchId: arg.match_id, disconnectedAt: new Date(), from: arg.from, to: arg.to }; console.log("####### TP_DISCONNECTED fired", summary) window.postMessage({ type: "TP_DISCONNECTED", data: summary }, "*"); } else if (maybeDisconnectFromMeLogArg) { console.log(" log detected as maybeDisconnectFromMeLogArg") pollUpdateScrapedConvoForLatestMatch() var partnerId = maybeDisconnectFromMeLogArg; if (DBLayer.latestKnownMatchId === null) throw new Error("a disconnected pushed form me without a latest registred match") /** * @type {DisconnectedArgs} */ const summary = { matchId: DBLayer.latestKnownMatchId, disconnectedAt: new Date(), from: null, to: partnerId }; console.log("####### TP_DISCONNECTED fired", summary) window.postMessage({ type: "TP_DISCONNECTED", data: summary }, "*"); } else if (experimentalPreDisconnectLog_) { console.log(" log detected as experimentalPreDisconnectLog_") pollUpdateScrapedConvoForLatestMatch() } else if (experimentalLefRoomLog_) { console.log(" log detected as experimentalLefRoomLog_") pollUpdateScrapedConvoForLatestMatch() } }; } /** * the part of injecting UI that relys on thundr UI and needs maintainance, only styling depends on thudr, the returned html structure does not change */ function injectUI_impl() { /** * @type {HTMLDivElement} */ const targetDiv = /**@type {HTMLDivElement}*/(document.evaluate("//div[@class='css-17vaizm']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue); var tpRoot = document.createElement("div") tpRoot.classList.add("css-1pum37") tpRoot.style.backgroundColor = "#00000066"; tpRoot.style.width = "640px" tpRoot.innerHTML = ` <div id="pt-panel-header" style="display: flex; flex-direction: row; justify-content: space-between; align-items: center; padding: 0; gap: 16px;"> <div style="display: flex; flex-direction: row; align-items: center; gap: 8px;"> <div style="display: flex; flex-direction: column; gap:0px;"> <style> /*todo: tp-nav-butn should be highlighted when hovered and should be grayed out when in disabled state*/ .tp-nav-btn { background: none; border: none; font-size: 16px; color: white; cursor: pointer; padding: 0; } .tp-nav-btn:hover:not(:disabled) { color: var(--chakra-colors-color-palette-solid); } .tp-nav-btn:disabled { color: #aaa; cursor: default; } </style> <button class="tp-nav-btn" id="tp-prev-btn">▲</button> <button class="tp-nav-btn" disabled id="tp-next-btn" style="">▼</button> </div> <div id="tp-cursor-status">4/7</div> </div> <div style="display: flex; flex-direction: column; align-items: flex-end; font-size:13px;"> <div id="tp-match-id-lbl">1254-5468-56548-54654</div> <div id="tp-connected-status" >Disconnected</div> </div> </div> <strong>ip:</strong> <span id="ip-span">-</span><br> <strong>desc:</strong><span id="ipNote-span">-</span><br> <strong>notes:</strong><span id="notes-span">-</span><br> <strong>info:</strong><span id="info-span">-</span><br> <strong>Convo count:</strong><span id="convoCount-span">-</span><br> <div style="display:flex; flex-direction:row; align-items:center; "> <textarea rows="2" cols="50" placeholder="Write ip notes..." id="tp-ipnote-textarea"></textarea> <button type="button" class="chakra-button css-18tsh74" id="tp-save-ipnote-btn">Save</button> </div> <br> <span id="toast-status">ready</span><br> <table id="history-table" style="width: 100%; border-collapse: collapse; margin-top: 10px; color: white; font-family: Arial, sans-serif; font-size:12px; min-width:640px;"> <thead> <tr> <th style="border: 1px solid white; padding: 4px;">Date</th> <th style="border: 1px solid white; padding: 4px;">Type</th> <th style="border: 1px solid white; padding: 4px;">Msg count</th> <th style="border: 1px solid white; padding: 4px;">Dur</th> <th style="border: 1px solid white; padding: 4px;">Lst. Msg</th> </tr> </thead> <tbody> <tr> <td style="border: 1px solid white; padding: 4px;">2025-04-23</td> <td style="border: 1px solid white; padding: 4px;">They skipped</td> <td style="border: 1px solid white; padding: 4px;">12</td> <td style="border: 1px solid white; padding: 4px;">5m 42s</td> <td style="border: 1px solid white; padding: 4px;">They: Thanks, got..!</td> </tr> </tbody> </table> <button type="button" class="chakra-button css-18tsh74 tp-toggle-convo-popup-btn" >Show/Hide Chat</button> <div id="tp-convo-popup" style=" position: absolute; display: flex; top: 80px; width: 100%; background-color: rgb(19, 4, 13);; max-height: 50vh; flex-direction: column;"> <button type="button" class="chakra-button css-18tsh74 tp-toggle-convo-popup-btn" style=" align-self: flex-end;">Show/Hide Chat</button> <div class="hide-scrollbar css-1qs2dh2 tp-convo-popup" style=" max-height: calc(100% - 40px); min-height: 0; "> </div> </div> `; targetDiv.appendChild(tpRoot) return tpRoot; } /** * * @returns {ConversationContent} */ function scrapeConversationContent() { /** * @type {ConversationContent} */ var res = [] /** * @type {HTMLDivElement} */ const chatRootDiv = /**@type {HTMLDivElement}*/(document.evaluate("//div[@class='css-1sl5lif']", document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue); var messagesDivs = Array.from(chatRootDiv.querySelectorAll(".css-wtl4b5")) messagesDivs.forEach(d => { var from =/**@type {"Stranger"|"You"}*/(d.querySelector("b")?.innerHTML) var content = "" d.childNodes.forEach(node => { if (node.nodeType === Node.TEXT_NODE) { content += node.textContent; } }); res.push({ from: from, content: content }) }) //div[@class='css-1sl5lif'] return res; } class Convo { constructor(partnerInfo) { this.partner = partnerInfo; this.messages = [] } } class TPUIComponent { /** * * @param {Interaction} interaction */ notifyInteractionAdded(interaction) { this.pages.forEach(p => { if (p.match.partnerInfo.ip == interaction.match.partnerInfo.ip) { p.interactions.push(interaction) if (this.pages[this._current] === p) { this.pushPartnerHistoryDetail(p.interactions, p.ipNote) } } }) } /** * @typedef {{match:Match,interactions:Interaction[], ipNote:string?,dirtyIpNote:string?,pageIndex:number,disconnectedArg:DisconnectedArgs?}} UIPage */ /** * @type {TPUIComponent} */ static mainPanel /** * * @param {HTMLDivElement} root */ constructor(root) { this._root = root; this._cursorStatusDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-cursor-status"))) this._connectedStatusDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-connected-status"))) this._navPrevBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-prev-btn"))) this._navNextBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-next-btn"))) this._tpMatchHeaderDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-match-id-lbl"))) this._ipNoteTextarea = (/**@type {HTMLTextAreaElement} */ (this._root.querySelector("#tp-ipnote-textarea"))) this._historyTable = (/**@type {HTMLTextAreaElement} */ (this._root.querySelector('#history-table'))) this._saveIpnoteBtn = (/**@type {HTMLButtonElement} */ (this._root.querySelector("#tp-save-ipnote-btn"))) this._toggleConvoPopupVisibilityBtns = (/**@type {HTMLButtonElement[]} */ (Array.from(this._root.querySelectorAll(".tp-toggle-convo-popup-btn")))) this._convoPopupDiv = (/**@type {HTMLDivElement} */ (this._root.querySelector("#tp-convo-popup"))) this._navPrevBtn.addEventListener("click", this._onPrevClick.bind(this)) this._navNextBtn.addEventListener("click", this._onNextClick.bind(this)) this._navNextBtn.addEventListener('contextmenu', (ev) => { ev.preventDefault(); this._onNextToEndClick(); return false; }, false); this._saveIpnoteBtn.addEventListener("click", this._onSaveIpnoteClick.bind(this)) this._toggleConvoPopupVisibilityBtns.forEach(b => { b.addEventListener("click", this._onToggleConvoPopupClick.bind(this)) }) this._ipNoteTextarea.addEventListener("input", this._onIpNoteInputChange.bind(this)); this.isAutoPaging = true; this._isTransparent = false; /** * @type {UIPage[]} */ this.pages = [] this._updateCursor(0) this._updateConvoPopupOpenStatus(false); this._root.querySelector("#pt-panel-header")?.addEventListener("dblclick", (ev) => { if (ev.srcElement !== this._root.querySelector("#pt-panel-header")) { return; } console.log(ev) this._isTransparent = !this._isTransparent; this._root.style.opacity = this._isTransparent ? "0.2" : "100%" }) } _updateConvoPopupOpenStatus(open) { this._isConvoPopupOpen = open; this._convoPopupDiv.style.display = open ? "flex" : "none"; } /** * * @param {Interaction} interaction */ _populateConvoPopupWithContent(interaction) { var container = /**@type {HTMLDivElement} */ (this._convoPopupDiv.querySelector(".css-1qs2dh2")); container.innerHTML = "" interaction.conversationContent.forEach(m => { var messageDiv = document.createElement("div") messageDiv.classList.add("css-wtl4b5") messageDiv.innerHTML = (m.from?.includes("Stranger")) ? `<b style="color: var(--chakra-colors-messages-stranger);">Stranger:</b>` : `<b style="color: var(--chakra-colors-messages-you);">You:</b>` messageDiv.appendChild(document.createTextNode(m.content)); container?.appendChild(messageDiv); }) } _onToggleConvoPopupClick() { this._updateConvoPopupOpenStatus(!this._isConvoPopupOpen); } _onSaveIpnoteClick() { //save to db and update dirty note and note in the page if (!this._getCurrentPage()) { return } console.log("saving ipnote", this._getCurrentPage().dirtyIpNote) DBLayer.DB.storeIPNote(this._getCurrentPage().match.partnerInfo.ip, this._getCurrentPage()?.dirtyIpNote || ""); this._getCurrentPage().ipNote = this._getCurrentPage().dirtyIpNote; this._refreshButtonsEnabledState() console.log("saved ipnote", this._getCurrentPage().dirtyIpNote) this.pushToast("saved note") } _onIpNoteInputChange() { if (!this._getCurrentPage()) { return; } var newNote = this._ipNoteTextarea.value this.pages[this._current].dirtyIpNote = newNote; this._refreshButtonsEnabledState() console.log("change detected", this._getCurrentPage()?.dirtyIpNote) } _updateCursor(current) { this._current = current; this._refreshButtonsEnabledState() this._refreshDisplayedCursor() } _refreshButtonsEnabledState() { this._navPrevBtn.disabled = !this._canClickPrev() this._navNextBtn.disabled = !this._canClickNext() this._saveIpnoteBtn.disabled = !this._canClickSaveIpnote() } _refreshDisplayedCursor() { if (this.pages.length > 0) { this._cursorStatusDiv.innerHTML = `${this._current + 1}/${this.pages.length}` } else { this._cursorStatusDiv.innerHTML = `(no matches)` } } _canClickPrev() { return (this.pages.length > 0 && this._current > 0) } _canClickNext() { return (this.pages.length > 0 && this._current < this.pages.length - 1) } _getCurrentPage() { return this.pages[this._current] } _canClickSaveIpnote() { if (!this._getCurrentPage()) { return false; } return this._getCurrentPage() && this._getCurrentPage().ipNote !== this._getCurrentPage().dirtyIpNote } _onPrevClick(ev) { ev.stopPropagation(); if (!this._canClickPrev()) return; this.displayPageOfIndex(this._current - 1) } _onNextClick(ev) { ev.stopPropagation(); if (!this._canClickNext()) return; this.displayPageOfIndex(this._current + 1) } _onNextToEndClick() { if (!this._canClickNext()) return; this.displayPageOfIndex(this.pages.length - 1) } /** * * @param {string} date * @param {*} type * @param {*} msgCount * @param {*} duration * @param {string?} lastMsg * @param {Interaction} interactionRef */ _addTableRow(date, type, msgCount, duration, lastMsg, interactionRef) { const table = this._historyTable.getElementsByTagName('tbody')[0]; const row = table.insertRow(); const cells = [date, type, msgCount, duration, lastMsg]; for (let i = 0; i < cells.length; i++) { const cell = row.insertCell(); if (i === 0) { //making the date clickable to open the chat popup cell.innerHTML = `<a href="javascript:;">${cells[i]}</a>` cell.querySelector("a")?.addEventListener("click", () => { this._populateConvoPopupWithContent(interactionRef) this._updateConvoPopupOpenStatus(true); }) } else if (i === 1) { cell.innerHTML = cells[i];//for advanced convo type styling (comes as html) } else if (i === 4) { const div = document.createElement("div"); div.textContent = lastMsg; div.title = cells[i]; Object.assign(div.style, { maxWidth: "100px", whiteSpace: "normal", overflow: "hidden", textOverflow: "ellipsis", display: "-webkit-box", WebkitLineClamp: "2", WebkitBoxOrient: "vertical" }); cell.textContent = ""; // Clear cell's default text cell.appendChild(div); } else { cell.textContent = cells[i]; } cell.style.border = "1px solid white"; cell.style.padding = "4px"; } } _clearTableRows() { const tbody = this._historyTable.getElementsByTagName('tbody')[0]; while (tbody.firstChild) { tbody.removeChild(tbody.firstChild); } } _toggleTableVisibility(visible) { this._historyTable.style.display = (!!visible) ? 'table' : 'none'; } /** * * @param {string} ip * @param {string} mId * @param {string} sex * @param {string} country */ _renderMatchDetails(ip, mId, sex, country) { (/**@type {HTMLSpanElement} */ (this._root.querySelector("#ip-span"))).innerHTML = ip; (/**@type {HTMLSpanElement} */ (this._root.querySelector("#info-span"))).innerHTML = `${sex?.toUpperCase() ?? "?"} - ${country?.toUpperCase()}`; this._tpMatchHeaderDiv.innerText = mId } /** * @param {string | number} convoCount * @param {Interaction?} lastInteraction * @param {string | null} [ipNote] */ _renderPartnerHistory(convoCount, lastInteraction, ipNote) { (/**@type {HTMLDivElement} */ (this._root.querySelector("#convoCount-span"))).innerHTML = convoCount?.toString() ?? "never"; if (convoCount) { (/**@type {HTMLDivElement} */ (this._root.querySelector("#convoCount-span"))).style.color = "green" } else { (/**@type {HTMLDivElement} */ (this._root.querySelector("#convoCount-span"))).style.color = "" } this._ipNoteTextarea.value = ipNote || ""; } _isInLastPageOrEmptyPage() { return (this.pages.length === 0) || (this._current == this.pages.length - 1); } pushPage(match, interactions, ipNote) { var shouldAutoPage = this.isAutoPaging && this._isInLastPageOrEmptyPage(); /** * @type {UIPage} */ var newPag = { match: match, interactions: interactions, ipNote: ipNote, dirtyIpNote: null, pageIndex: this.pages.length, disconnectedArg: null } this.pages.push(newPag) if (shouldAutoPage) { this._updateCursor(this._current + 1) this.displayPageOfIndex(this._current) } else { this._updateCursor(this._current) } } _refreshPageConnectinStatus() { if (!this._getCurrentPage()) { this._connectedStatusDiv.innerText = "" return; } this._connectedStatusDiv.innerText = this._getCurrentPage().disconnectedArg ? "Disconnected" : "Connected"; } _updatePageConnectionStatus(matchId, disconnectedArgs) { this.pages.forEach(p => { if (p.match.matchId === matchId) { p.disconnectedArg = disconnectedArgs } }) this._refreshPageConnectinStatus() } displayPageOfIndex(ix) { if (ix > (this.pages.length - 1)) { console.log("requested to diplay out of range page ", ix, this.pages) return; } var page = this.pages[ix] this.clear() this.pushMatch(page.match) this.pushPartnerHistoryDetail(page.interactions, page.ipNote) if (page.dirtyIpNote !== page.ipNote) { this._ipNoteTextarea.value = page.dirtyIpNote || "" } this._updateCursor(ix) this._refreshPageConnectinStatus() } clear() { this._clearTableRows(); this._toggleTableVisibility(false) } /** * * @param {Match} match */ pushMatch(match) { this._renderMatchDetails(match.partnerInfo.ip, match.matchId, match.partnerInfo.sex, match.partnerInfo.country) } /** * * @param {Interaction[]?} interactions * @param {string?} ipNote */ pushPartnerHistoryDetail(interactions, ipNote) { console.log("push history ", interactions) this._clearTableRows() function truncateString(str) { return str.length > 20 ? str.slice(0, 19) + '…' : str; } if (interactions === null) { this._renderPartnerHistory("-", null, null) return; } var latestInteraction = interactions.length == 0 ? null : interactions.sort((a, b) => new Date(a.match.matchedAt).getTime() - new Date(b.match.matchedAt).getTime())[0] this._renderPartnerHistory(interactions.length, latestInteraction, ipNote) if (interactions && interactions.length) { this._toggleTableVisibility(true) interactions.forEach(i => { var msgCc = i.conversationContent.length var dur = getDurationString(i?.match.matchedAt, i?.disconnectedAt); var fullMessage = i.conversationContent.length > 0 ? (i.conversationContent[i.conversationContent.length - 1].from + i.conversationContent[i.conversationContent.length - 1].content) : null const date = new Date(i?.match.matchedAt); const friendlyDateTime = date.toLocaleString('en-GB', { day: '2-digit', month: 'long', year: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false }); this._addTableRow(friendlyDateTime, getInteractionType(i), msgCc, dur, fullMessage, i) }) } else { this._toggleTableVisibility(false) } } onCloseConvoClick() { } /** * * @param {string} message */ pushToast(message) { (/**@type {HTMLSpanElement} */ (this._root.querySelector("#toast-status"))).innerText = message; } } /** * set up the TP UI parts and injects them in the html and have the various standard events and TP UI events integrated, * this contains most of the app logic */ function initTPUI() { if (window["tp_initTPUI_called"]) { throw new Error("initTPUI already called") } window["tp_initTPUI_called"] = true var tpRoot = injectUI_impl() TPUIComponent.mainPanel = new TPUIComponent(tpRoot) var testIp = "yassinMi" TPUIComponent.mainPanel.pushMatch({ matchedAt: new Date(), matchId: "yass", partnerInfo: { ip: testIp, country: "ma", sex: "M", id: "111" } }) TPUIComponent.mainPanel.pushPartnerHistoryDetail([], null) window.addEventListener("message", (event) => { if (event.source !== window) return; if (event.data.type === "TP_MESSAGE") { } if (event.data.type === "TP_MATCHED") { /** * @type {Match} */ const m = event.data.data console.log("matched recieved,", m) var interactions = DBLayer.DB.getInteractionssByIP(m.partnerInfo.ip); var ipNote = DBLayer.DB.getIPNote(m.partnerInfo.ip) TPUIComponent.mainPanel.pushPage(m, interactions, ipNote) DBLayer.verifyReadyForNewMatchEntry() DBLayer.matchesBuffer.push({ match: m, matchRaw: null, disconnectArgs: null, scrapedConvo: null }) DBLayer.latestKnownMatchId = m.matchId } if (event.data.type === "TP_DISCONNECTED") { /** * @type {DisconnectedArgs} */ const disconnectedArgs = event.data.data; pollUpdateScrapedConvoForLatestMatch() var matchEntry if (disconnectedArgs.from === null) { console.log("handeling diconnection from pushed") var latestMatchEntry = DBLayer.latestKnownMatchId === null ? null : DBLayer.getMatchFromMatchBuffer(DBLayer.latestKnownMatchId) if (!latestMatchEntry) { throw new Error("a pushed disconnected without existing current match entry") } if (latestMatchEntry.match.partnerInfo.id !== disconnectedArgs.to) { throw new Error("a pushed disconnected without existing current match entry matching the destinated partner") } matchEntry = latestMatchEntry; } else { console.log("handeling diconnection from user") matchEntry = DBLayer.getMatchFromMatchBuffer(disconnectedArgs.matchId) if (matchEntry === undefined) { throw new Error("cannot find match with id form disconnected args") } } if (matchEntry.disconnectArgs) { if (matchEntry.disconnectArgs.to !== "") throw new Error("the match entry is already disconnected properly") else { throw new Error("the match entry is already disconnected implecitly") } } matchEntry.disconnectArgs = disconnectedArgs; console.log("marked match entry as disconnected") TPUIComponent.mainPanel.pushToast(`disconnected from ${disconnectedArgs.matchId}`); /** * @type {"Stranger"|"You"} */ var disconnectedBy; if (matchEntry.match.partnerInfo.id == disconnectedArgs.from) { disconnectedBy = "Stranger" } else if (matchEntry.match.partnerInfo.id === disconnectedArgs.to) { disconnectedBy = "You" } else { throw new Error("disconnected args from and to do not match partner id") } console.log(`the disconnected match entry had scraped convo of ${matchEntry.scrapedConvo?.length}`) /** * @type {Interaction} */ var interaction = { conversationContent: matchEntry.scrapedConvo || [], disconnectedBy: disconnectedBy, match: matchEntry.match, disconnectedAt: disconnectedArgs.disconnectedAt } DBLayer.DB.addInteraction(interaction) TPUIComponent.mainPanel.notifyInteractionAdded(interaction) TPUIComponent.mainPanel._updatePageConnectionStatus(interaction.match.matchId, matchEntry.disconnectArgs) } }); } (function () { console.log("tp script started") var initialized = false hookEvents(); window.addEventListener("message", (event) => { if (initialized) return; if (event.source !== window) return; if (event.data.type === "TP_INIT") { initialized = true initTPUI(); var pollingScrap = setInterval(() => { pollUpdateScrapedConvoForLatestMatch() }, 10000); } }) })(); class AnalyticsHelper { static function(params) { } } function pollUpdateScrapedConvoForLatestMatch() { try { if (DBLayer.latestKnownMatchId) { DBLayer.DB.updateScrapedConvoForMatchId(DBLayer.latestKnownMatchId, scrapeConversationContent()) } } catch (e) { console.error(e); } } function getDurationString(startDate, endDate) { let diffMs = Math.abs(new Date(endDate).getTime() - new Date(startDate).getTime()); let seconds = Math.floor(diffMs / 1000) % 60; let minutes = Math.floor(diffMs / (1000 * 60)) % 60; let hours = Math.floor(diffMs / (1000 * 60 * 60)); return `${hours}h ${minutes}m ${seconds}s`; } /** * Returns a colored label with interaction type and who skipped. * @param {Interaction} interaction */ function getInteractionType(interaction) { const whoSkipped = interaction.disconnectedBy === "Stranger" ? "they skipped" : "you skipped"; const msgCount = interaction.conversationContent.length; if (msgCount === 0) { return `<span style="color:crimson;">empty — ${whoSkipped}</span>`; } if (msgCount < 10) { const color = interaction.disconnectedBy === "Stranger" ? "orangered" : "tomato"; return `<span style="color:${color};">short — ${whoSkipped}</span>`; } const color = interaction.disconnectedBy === "Stranger" ? "mediumseagreen" : "limegreen"; return `<span style="color:${color};">meaningful — ${whoSkipped}</span>`; }
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址