Greasy Fork

Greasy Fork is available in English.

Yeah! for Twitter

Adds Yeah! button to Twitter, essentially a public Like

当前为 2024-06-16 提交的版本,查看 最新版本

// ==UserScript==
// @name         Yeah! for Twitter
// @namespace    http://tampermonkey.net/
// @version      1.0.5
// @description  Adds Yeah! button to Twitter, essentially a public Like
// @author       dimden.dev
// @match        https://x.com/*
// @match        https://twitter.com/*
// @icon         https://dimden.dev/images/yeah_logo.png
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// ==/UserScript==

// fetch polyfill
function GM_fetch(url, options = {}) {
    return new Promise((resolve, reject) => {
        const method = options.method || 'GET';
        const headers = options.headers || {};
        const body = options.body || null;

        GM_xmlhttpRequest({
            method,
            url,
            headers,
            data: body,
            onload(response) {
                const responseBody = response.responseText;
                const status = response.status;
                const statusText = response.statusText;
                const responseHeaders = parseHeaders(response.responseHeaders);

                resolve(new Response(responseBody, {
                    status,
                    statusText,
                    headers: responseHeaders
                }));
            },
            onerror(error) {
                reject(new Error('Network request failed'));
            },
            ontimeout() {
                reject(new Error('Network request timed out'));
            }
        });
    });
}

function parseHeaders(headersString) {
    const headers = new Headers();
    const lines = headersString.trim().split(/[\r\n]+/);
    lines.forEach(line => {
        const parts = line.split(': ');
        const header = parts.shift();
        const value = parts.join(': ');
        headers.append(header, value);
    });
    return headers;
}

class Response {
    constructor(body, options) {
        this.body = body;
        this.status = options.status;
        this.statusText = options.statusText;
        this.headers = options.headers;
    }

    text() {
        return Promise.resolve(this.body);
    }

    json() {
        return Promise.resolve(JSON.parse(this.body));
    }
}

// chrome.storage.local polyfill
window.chrome = window.chrome || {};
chrome.runtime = chrome.runtime || {id: 'userscript'};
chrome.storage = chrome.storage || {};
chrome.storage.local = {
    storageKey: 'chromeStorage',

    _getStorageObject: function () {
        const storage = localStorage.getItem(this.storageKey);
        return storage ? JSON.parse(storage) : {};
    },

    _setStorageObject: function (obj) {
        localStorage.setItem(this.storageKey, JSON.stringify(obj));
    },

    get: function (keys, callback) {
        const storageObj = this._getStorageObject();
        const result = {};

        if (typeof keys === 'string') {
            result[keys] = storageObj[keys];
        } else if (Array.isArray(keys)) {
            keys.forEach(key => {
                result[key] = storageObj[key];
            });
        } else if (typeof keys === 'object') {
            Object.keys(keys).forEach(key => {
                result[key] = storageObj[key] !== undefined ? storageObj[key] : keys[key];
            });
        } else {
            Object.assign(result, storageObj);
        }

        callback(result);
    },

    set: function (items, callback) {
        const storageObj = this._getStorageObject();

        Object.keys(items).forEach(key => {
            storageObj[key] = items[key];
        });

        this._setStorageObject(storageObj);

        if (callback) callback();
    },

    remove: function (keys, callback) {
        const storageObj = this._getStorageObject();

        if (typeof keys === 'string') {
            delete storageObj[keys];
        } else if (Array.isArray(keys)) {
            keys.forEach(key => {
                delete storageObj[key];
            });
        }

        this._setStorageObject(storageObj);

        if (callback) callback();
    },

    clear: function (callback) {
        localStorage.removeItem(this.storageKey);
        if (callback) callback();
    }
};

GM_registerMenuCommand("Don't like tweet on Yeah", function () {
    chrome.storage.local.set({
        settings: {
            dontLike: true
        }
    });
});

GM_registerMenuCommand("Like tweet on Yeah", function () {
    chrome.storage.local.set({
        settings: {
            dontLike: false
        }
    });
});

GM_registerMenuCommand("Clear account tokens", function () {
    chrome.storage.local.remove(['yeahToken', 'yeahTokens']);
});

GM_registerMenuCommand("Reset popup settings", function () {
    chrome.storage.local.remove(['ignorePopup']);
});

// scripts/purify.min.js
/*! @license DOMPurify 3.1.3 | (c) Cure53 and other contributors | Released under the Apache license 2.0 and Mozilla Public License 2.0 | github.com/cure53/DOMPurify/blob/3.1.3/LICENSE */
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self).DOMPurify=t()}(this,(function(){"use strict";const{entries:e,setPrototypeOf:t,isFrozen:n,getPrototypeOf:o,getOwnPropertyDescriptor:r}=Object;let{freeze:i,seal:a,create:l}=Object,{apply:c,construct:s}="undefined"!=typeof Reflect&&Reflect;i||(i=function(e){return e}),a||(a=function(e){return e}),c||(c=function(e,t,n){return e.apply(t,n)}),s||(s=function(e,t){return new e(...t)});const u=S(Array.prototype.forEach),m=S(Array.prototype.pop),p=S(Array.prototype.push),f=S(String.prototype.toLowerCase),d=S(String.prototype.toString),h=S(String.prototype.match),g=S(String.prototype.replace),_=S(String.prototype.indexOf),T=S(String.prototype.trim),y=S(Object.prototype.hasOwnProperty),E=S(RegExp.prototype.test),A=(N=TypeError,function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return s(N,t)});var N;const b=S(Number.isNaN);function S(e){return function(t){for(var n=arguments.length,o=new Array(n>1?n-1:0),r=1;r<n;r++)o[r-1]=arguments[r];return c(e,t,o)}}function R(e,o){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:f;t&&t(e,null);let i=o.length;for(;i--;){let t=o[i];if("string"==typeof t){const e=r(t);e!==t&&(n(o)||(o[i]=e),t=e)}e[t]=!0}return e}function w(e){for(let t=0;t<e.length;t++){y(e,t)||(e[t]=null)}return e}function C(t){const n=l(null);for(const[o,r]of e(t)){y(t,o)&&(Array.isArray(r)?n[o]=w(r):r&&"object"==typeof r&&r.constructor===Object?n[o]=C(r):n[o]=r)}return n}function v(e,t){for(;null!==e;){const n=r(e,t);if(n){if(n.get)return S(n.get);if("function"==typeof n.value)return S(n.value)}e=o(e)}return function(){return null}}const L=i(["a","abbr","acronym","address","area","article","aside","audio","b","bdi","bdo","big","blink","blockquote","body","br","button","canvas","caption","center","cite","code","col","colgroup","content","data","datalist","dd","decorator","del","details","dfn","dialog","dir","div","dl","dt","element","em","fieldset","figcaption","figure","font","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","img","input","ins","kbd","label","legend","li","main","map","mark","marquee","menu","menuitem","meter","nav","nobr","ol","optgroup","option","output","p","picture","pre","progress","q","rp","rt","ruby","s","samp","section","select","shadow","small","source","spacer","span","strike","strong","style","sub","summary","sup","table","tbody","td","template","textarea","tfoot","th","thead","time","tr","track","tt","u","ul","var","video","wbr"]),D=i(["svg","a","altglyph","altglyphdef","altglyphitem","animatecolor","animatemotion","animatetransform","circle","clippath","defs","desc","ellipse","filter","font","g","glyph","glyphref","hkern","image","line","lineargradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialgradient","rect","stop","style","switch","symbol","text","textpath","title","tref","tspan","view","vkern"]),O=i(["feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence"]),x=i(["animate","color-profile","cursor","discard","font-face","font-face-format","font-face-name","font-face-src","font-face-uri","foreignobject","hatch","hatchpath","mesh","meshgradient","meshpatch","meshrow","missing-glyph","script","set","solidcolor","unknown","use"]),k=i(["math","menclose","merror","mfenced","mfrac","mglyph","mi","mlabeledtr","mmultiscripts","mn","mo","mover","mpadded","mphantom","mroot","mrow","ms","mspace","msqrt","mstyle","msub","msup","msubsup","mtable","mtd","mtext","mtr","munder","munderover","mprescripts"]),M=i(["maction","maligngroup","malignmark","mlongdiv","mscarries","mscarry","msgroup","mstack","msline","msrow","semantics","annotation","annotation-xml","mprescripts","none"]),I=i(["#text"]),U=i(["accept","action","align","alt","autocapitalize","autocomplete","autopictureinpicture","autoplay","background","bgcolor","border","capture","cellpadding","cellspacing","checked","cite","class","clear","color","cols","colspan","controls","controlslist","coords","crossorigin","datetime","decoding","default","dir","disabled","disablepictureinpicture","disableremoteplayback","download","draggable","enctype","enterkeyhint","face","for","headers","height","hidden","high","href","hreflang","id","inputmode","integrity","ismap","kind","label","lang","list","loading","loop","low","max","maxlength","media","method","min","minlength","multiple","muted","name","nonce","noshade","novalidate","nowrap","open","optimum","pattern","placeholder","playsinline","poster","preload","pubdate","radiogroup","readonly","rel","required","rev","reversed","role","rows","rowspan","spellcheck","scope","selected","shape","size","sizes","span","srclang","start","src","srcset","step","style","summary","tabindex","title","translate","type","usemap","valign","value","width","wrap","xmlns","slot"]),P=i(["accent-height","accumulate","additive","alignment-baseline","ascent","attributename","attributetype","azimuth","basefrequency","baseline-shift","begin","bias","by","class","clip","clippathunits","clip-path","clip-rule","color","color-interpolation","color-interpolation-filters","color-profile","color-rendering","cx","cy","d","dx","dy","diffuseconstant","direction","display","divisor","dur","edgemode","elevation","end","fill","fill-opacity","fill-rule","filter","filterunits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","fx","fy","g1","g2","glyph-name","glyphref","gradientunits","gradienttransform","height","href","id","image-rendering","in","in2","k","k1","k2","k3","k4","kerning","keypoints","keysplines","keytimes","lang","lengthadjust","letter-spacing","kernelmatrix","kernelunitlength","lighting-color","local","marker-end","marker-mid","marker-start","markerheight","markerunits","markerwidth","maskcontentunits","maskunits","max","mask","media","method","mode","min","name","numoctaves","offset","operator","opacity","order","orient","orientation","origin","overflow","paint-order","path","pathlength","patterncontentunits","patterntransform","patternunits","points","preservealpha","preserveaspectratio","primitiveunits","r","rx","ry","radius","refx","refy","repeatcount","repeatdur","restart","result","rotate","scale","seed","shape-rendering","specularconstant","specularexponent","spreadmethod","startoffset","stddeviation","stitchtiles","stop-color","stop-opacity","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke","stroke-width","style","surfacescale","systemlanguage","tabindex","targetx","targety","transform","transform-origin","text-anchor","text-decoration","text-rendering","textlength","type","u1","u2","unicode","values","viewbox","visibility","version","vert-adv-y","vert-origin-x","vert-origin-y","width","word-spacing","wrap","writing-mode","xchannelselector","ychannelselector","x","x1","x2","xmlns","y","y1","y2","z","zoomandpan"]),F=i(["accent","accentunder","align","bevelled","close","columnsalign","columnlines","columnspan","denomalign","depth","dir","display","displaystyle","encoding","fence","frame","height","href","id","largeop","length","linethickness","lspace","lquote","mathbackground","mathcolor","mathsize","mathvariant","maxsize","minsize","movablelimits","notation","numalign","open","rowalign","rowlines","rowspacing","rowspan","rspace","rquote","scriptlevel","scriptminsize","scriptsizemultiplier","selection","separator","separators","stretchy","subscriptshift","supscriptshift","symmetric","voffset","width","xmlns"]),H=i(["xlink:href","xml:id","xlink:title","xml:space","xmlns:xlink"]),z=a(/\{\{[\w\W]*|[\w\W]*\}\}/gm),B=a(/<%[\w\W]*|[\w\W]*%>/gm),W=a(/\${[\w\W]*}/gm),G=a(/^data-[\-\w.\u00B7-\uFFFF]/),Y=a(/^aria-[\-\w]+$/),j=a(/^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i),X=a(/^(?:\w+script|data):/i),q=a(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g),$=a(/^html$/i),K=a(/^[a-z][.\w]*(-[.\w]+)+$/i);var V=Object.freeze({__proto__:null,MUSTACHE_EXPR:z,ERB_EXPR:B,TMPLIT_EXPR:W,DATA_ATTR:G,ARIA_ATTR:Y,IS_ALLOWED_URI:j,IS_SCRIPT_OR_DATA:X,ATTR_WHITESPACE:q,DOCTYPE_NAME:$,CUSTOM_ELEMENT:K});const Z=1,J=3,Q=7,ee=8,te=9,ne=function(){return"undefined"==typeof window?null:window},oe=function(e,t){if("object"!=typeof e||"function"!=typeof e.createPolicy)return null;let n=null;const o="data-tt-policy-suffix";t&&t.hasAttribute(o)&&(n=t.getAttribute(o));const r="dompurify"+(n?"#"+n:"");try{return e.createPolicy(r,{createHTML:e=>e,createScriptURL:e=>e})}catch(e){return console.warn("TrustedTypes policy "+r+" could not be created."),null}};var re=function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:ne();const o=e=>t(e);if(o.version="3.1.3",o.removed=[],!n||!n.document||n.document.nodeType!==te)return o.isSupported=!1,o;let{document:r}=n;const a=r,c=a.currentScript,{DocumentFragment:s,HTMLTemplateElement:N,Node:S,Element:w,NodeFilter:z,NamedNodeMap:B=n.NamedNodeMap||n.MozNamedAttrMap,HTMLFormElement:W,DOMParser:G,trustedTypes:Y}=n,X=w.prototype,q=v(X,"cloneNode"),K=v(X,"nextSibling"),re=v(X,"childNodes"),ie=v(X,"parentNode");if("function"==typeof N){const e=r.createElement("template");e.content&&e.content.ownerDocument&&(r=e.content.ownerDocument)}let ae,le="";const{implementation:ce,createNodeIterator:se,createDocumentFragment:ue,getElementsByTagName:me}=r,{importNode:pe}=a;let fe={};o.isSupported="function"==typeof e&&"function"==typeof ie&&ce&&void 0!==ce.createHTMLDocument;const{MUSTACHE_EXPR:de,ERB_EXPR:he,TMPLIT_EXPR:ge,DATA_ATTR:_e,ARIA_ATTR:Te,IS_SCRIPT_OR_DATA:ye,ATTR_WHITESPACE:Ee,CUSTOM_ELEMENT:Ae}=V;let{IS_ALLOWED_URI:Ne}=V,be=null;const Se=R({},[...L,...D,...O,...k,...I]);let Re=null;const we=R({},[...U,...P,...F,...H]);let Ce=Object.seal(l(null,{tagNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},attributeNameCheck:{writable:!0,configurable:!1,enumerable:!0,value:null},allowCustomizedBuiltInElements:{writable:!0,configurable:!1,enumerable:!0,value:!1}})),ve=null,Le=null,De=!0,Oe=!0,xe=!1,ke=!0,Me=!1,Ie=!0,Ue=!1,Pe=!1,Fe=!1,He=!1,ze=!1,Be=!1,We=!0,Ge=!1;const Ye="user-content-";let je=!0,Xe=!1,qe={},$e=null;const Ke=R({},["annotation-xml","audio","colgroup","desc","foreignobject","head","iframe","math","mi","mn","mo","ms","mtext","noembed","noframes","noscript","plaintext","script","style","svg","template","thead","title","video","xmp"]);let Ve=null;const Ze=R({},["audio","video","img","source","image","track"]);let Je=null;const Qe=R({},["alt","class","for","id","label","name","pattern","placeholder","role","summary","title","value","style","xmlns"]),et="http://www.w3.org/1998/Math/MathML",tt="http://www.w3.org/2000/svg",nt="http://www.w3.org/1999/xhtml";let ot=nt,rt=!1,it=null;const at=R({},[et,tt,nt],d);let lt=null;const ct=["application/xhtml+xml","text/html"],st="text/html";let ut=null,mt=null;const pt=255,ft=r.createElement("form"),dt=function(e){return e instanceof RegExp||e instanceof Function},ht=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};if(!mt||mt!==e){if(e&&"object"==typeof e||(e={}),e=C(e),lt=-1===ct.indexOf(e.PARSER_MEDIA_TYPE)?st:e.PARSER_MEDIA_TYPE,ut="application/xhtml+xml"===lt?d:f,be=y(e,"ALLOWED_TAGS")?R({},e.ALLOWED_TAGS,ut):Se,Re=y(e,"ALLOWED_ATTR")?R({},e.ALLOWED_ATTR,ut):we,it=y(e,"ALLOWED_NAMESPACES")?R({},e.ALLOWED_NAMESPACES,d):at,Je=y(e,"ADD_URI_SAFE_ATTR")?R(C(Qe),e.ADD_URI_SAFE_ATTR,ut):Qe,Ve=y(e,"ADD_DATA_URI_TAGS")?R(C(Ze),e.ADD_DATA_URI_TAGS,ut):Ze,$e=y(e,"FORBID_CONTENTS")?R({},e.FORBID_CONTENTS,ut):Ke,ve=y(e,"FORBID_TAGS")?R({},e.FORBID_TAGS,ut):{},Le=y(e,"FORBID_ATTR")?R({},e.FORBID_ATTR,ut):{},qe=!!y(e,"USE_PROFILES")&&e.USE_PROFILES,De=!1!==e.ALLOW_ARIA_ATTR,Oe=!1!==e.ALLOW_DATA_ATTR,xe=e.ALLOW_UNKNOWN_PROTOCOLS||!1,ke=!1!==e.ALLOW_SELF_CLOSE_IN_ATTR,Me=e.SAFE_FOR_TEMPLATES||!1,Ie=!1!==e.SAFE_FOR_XML,Ue=e.WHOLE_DOCUMENT||!1,He=e.RETURN_DOM||!1,ze=e.RETURN_DOM_FRAGMENT||!1,Be=e.RETURN_TRUSTED_TYPE||!1,Fe=e.FORCE_BODY||!1,We=!1!==e.SANITIZE_DOM,Ge=e.SANITIZE_NAMED_PROPS||!1,je=!1!==e.KEEP_CONTENT,Xe=e.IN_PLACE||!1,Ne=e.ALLOWED_URI_REGEXP||j,ot=e.NAMESPACE||nt,Ce=e.CUSTOM_ELEMENT_HANDLING||{},e.CUSTOM_ELEMENT_HANDLING&&dt(e.CUSTOM_ELEMENT_HANDLING.tagNameCheck)&&(Ce.tagNameCheck=e.CUSTOM_ELEMENT_HANDLING.tagNameCheck),e.CUSTOM_ELEMENT_HANDLING&&dt(e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck)&&(Ce.attributeNameCheck=e.CUSTOM_ELEMENT_HANDLING.attributeNameCheck),e.CUSTOM_ELEMENT_HANDLING&&"boolean"==typeof e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements&&(Ce.allowCustomizedBuiltInElements=e.CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements),Me&&(Oe=!1),ze&&(He=!0),qe&&(be=R({},I),Re=[],!0===qe.html&&(R(be,L),R(Re,U)),!0===qe.svg&&(R(be,D),R(Re,P),R(Re,H)),!0===qe.svgFilters&&(R(be,O),R(Re,P),R(Re,H)),!0===qe.mathMl&&(R(be,k),R(Re,F),R(Re,H))),e.ADD_TAGS&&(be===Se&&(be=C(be)),R(be,e.ADD_TAGS,ut)),e.ADD_ATTR&&(Re===we&&(Re=C(Re)),R(Re,e.ADD_ATTR,ut)),e.ADD_URI_SAFE_ATTR&&R(Je,e.ADD_URI_SAFE_ATTR,ut),e.FORBID_CONTENTS&&($e===Ke&&($e=C($e)),R($e,e.FORBID_CONTENTS,ut)),je&&(be["#text"]=!0),Ue&&R(be,["html","head","body"]),be.table&&(R(be,["tbody"]),delete ve.tbody),e.TRUSTED_TYPES_POLICY){if("function"!=typeof e.TRUSTED_TYPES_POLICY.createHTML)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.');if("function"!=typeof e.TRUSTED_TYPES_POLICY.createScriptURL)throw A('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.');ae=e.TRUSTED_TYPES_POLICY,le=ae.createHTML("")}else void 0===ae&&(ae=oe(Y,c)),null!==ae&&"string"==typeof le&&(le=ae.createHTML(""));i&&i(e),mt=e}},gt=R({},["mi","mo","mn","ms","mtext"]),_t=R({},["foreignobject","annotation-xml"]),Tt=R({},["title","style","font","a","script"]),yt=R({},[...D,...O,...x]),Et=R({},[...k,...M]),At=function(e){let t=ie(e);t&&t.tagName||(t={namespaceURI:ot,tagName:"template"});const n=f(e.tagName),o=f(t.tagName);return!!it[e.namespaceURI]&&(e.namespaceURI===tt?t.namespaceURI===nt?"svg"===n:t.namespaceURI===et?"svg"===n&&("annotation-xml"===o||gt[o]):Boolean(yt[n]):e.namespaceURI===et?t.namespaceURI===nt?"math"===n:t.namespaceURI===tt?"math"===n&&_t[o]:Boolean(Et[n]):e.namespaceURI===nt?!(t.namespaceURI===tt&&!_t[o])&&(!(t.namespaceURI===et&&!gt[o])&&(!Et[n]&&(Tt[n]||!yt[n]))):!("application/xhtml+xml"!==lt||!it[e.namespaceURI]))},Nt=function(e){p(o.removed,{element:e});try{e.parentNode.removeChild(e)}catch(t){e.remove()}},bt=function(e,t){try{p(o.removed,{attribute:t.getAttributeNode(e),from:t})}catch(e){p(o.removed,{attribute:null,from:t})}if(t.removeAttribute(e),"is"===e&&!Re[e])if(He||ze)try{Nt(t)}catch(e){}else try{t.setAttribute(e,"")}catch(e){}},St=function(e){let t=null,n=null;if(Fe)e="<remove></remove>"+e;else{const t=h(e,/^[\r\n\t ]+/);n=t&&t[0]}"application/xhtml+xml"===lt&&ot===nt&&(e='<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>'+e+"</body></html>");const o=ae?ae.createHTML(e):e;if(ot===nt)try{t=(new G).parseFromString(o,lt)}catch(e){}if(!t||!t.documentElement){t=ce.createDocument(ot,"template",null);try{t.documentElement.innerHTML=rt?le:o}catch(e){}}const i=t.body||t.documentElement;return e&&n&&i.insertBefore(r.createTextNode(n),i.childNodes[0]||null),ot===nt?me.call(t,Ue?"html":"body")[0]:Ue?t.documentElement:i},Rt=function(e){return se.call(e.ownerDocument||e,e,z.SHOW_ELEMENT|z.SHOW_COMMENT|z.SHOW_TEXT|z.SHOW_PROCESSING_INSTRUCTION|z.SHOW_CDATA_SECTION,null)},wt=function(e){return e instanceof W&&(void 0!==e.__depth&&"number"!=typeof e.__depth||void 0!==e.__removalCount&&"number"!=typeof e.__removalCount||"string"!=typeof e.nodeName||"string"!=typeof e.textContent||"function"!=typeof e.removeChild||!(e.attributes instanceof B)||"function"!=typeof e.removeAttribute||"function"!=typeof e.setAttribute||"string"!=typeof e.namespaceURI||"function"!=typeof e.insertBefore||"function"!=typeof e.hasChildNodes)},Ct=function(e){return"function"==typeof S&&e instanceof S},vt=function(e,t,n){fe[e]&&u(fe[e],(e=>{e.call(o,t,n,mt)}))},Lt=function(e){let t=null;if(vt("beforeSanitizeElements",e,null),wt(e))return Nt(e),!0;const n=ut(e.nodeName);if(vt("uponSanitizeElement",e,{tagName:n,allowedTags:be}),e.hasChildNodes()&&!Ct(e.firstElementChild)&&E(/<[/\w]/g,e.innerHTML)&&E(/<[/\w]/g,e.textContent))return Nt(e),!0;if(e.nodeType===Q)return Nt(e),!0;if(Ie&&e.nodeType===ee&&E(/<[/\w]/g,e.data))return Nt(e),!0;if(!be[n]||ve[n]){if(!ve[n]&&Ot(n)){if(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,n))return!1;if(Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))return!1}if(je&&!$e[n]){const t=ie(e)||e.parentNode,n=re(e)||e.childNodes;if(n&&t){for(let o=n.length-1;o>=0;--o){const r=q(n[o],!0);r.__removalCount=(e.__removalCount||0)+1,t.insertBefore(r,K(e))}}}return Nt(e),!0}return e instanceof w&&!At(e)?(Nt(e),!0):"noscript"!==n&&"noembed"!==n&&"noframes"!==n||!E(/<\/no(script|embed|frames)/i,e.innerHTML)?(Me&&e.nodeType===J&&(t=e.textContent,u([de,he,ge],(e=>{t=g(t,e," ")})),e.textContent!==t&&(p(o.removed,{element:e.cloneNode()}),e.textContent=t)),vt("afterSanitizeElements",e,null),!1):(Nt(e),!0)},Dt=function(e,t,n){if(We&&("id"===t||"name"===t)&&(n in r||n in ft||"__depth"===n||"__removalCount"===n))return!1;if(Oe&&!Le[t]&&E(_e,t));else if(De&&E(Te,t));else if(!Re[t]||Le[t]){if(!(Ot(e)&&(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,e)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(e))&&(Ce.attributeNameCheck instanceof RegExp&&E(Ce.attributeNameCheck,t)||Ce.attributeNameCheck instanceof Function&&Ce.attributeNameCheck(t))||"is"===t&&Ce.allowCustomizedBuiltInElements&&(Ce.tagNameCheck instanceof RegExp&&E(Ce.tagNameCheck,n)||Ce.tagNameCheck instanceof Function&&Ce.tagNameCheck(n))))return!1}else if(Je[t]);else if(E(Ne,g(n,Ee,"")));else if("src"!==t&&"xlink:href"!==t&&"href"!==t||"script"===e||0!==_(n,"data:")||!Ve[e]){if(xe&&!E(ye,g(n,Ee,"")));else if(n)return!1}else;return!0},Ot=function(e){return"annotation-xml"!==e&&h(e,Ae)},xt=function(e){vt("beforeSanitizeAttributes",e,null);const{attributes:t}=e;if(!t)return;const n={attrName:"",attrValue:"",keepAttr:!0,allowedAttributes:Re};let r=t.length;for(;r--;){const i=t[r],{name:a,namespaceURI:l,value:c}=i,s=ut(a);let p="value"===a?c:T(c);if(n.attrName=s,n.attrValue=p,n.keepAttr=!0,n.forceKeepAttr=void 0,vt("uponSanitizeAttribute",e,n),p=n.attrValue,n.forceKeepAttr)continue;if(bt(a,e),!n.keepAttr)continue;if(!ke&&E(/\/>/i,p)){bt(a,e);continue}if(Ie&&E(/((--!?|])>)|<\/(style|title)/i,p)){bt(a,e);continue}Me&&u([de,he,ge],(e=>{p=g(p,e," ")}));const f=ut(e.nodeName);if(Dt(f,s,p)){if(!Ge||"id"!==s&&"name"!==s||(bt(a,e),p=Ye+p),ae&&"object"==typeof Y&&"function"==typeof Y.getAttributeType)if(l);else switch(Y.getAttributeType(f,s)){case"TrustedHTML":p=ae.createHTML(p);break;case"TrustedScriptURL":p=ae.createScriptURL(p)}try{l?e.setAttributeNS(l,a,p):e.setAttribute(a,p),wt(e)?Nt(e):m(o.removed)}catch(e){}}}vt("afterSanitizeAttributes",e,null)},kt=function e(t){let n=null;const o=Rt(t);for(vt("beforeSanitizeShadowDOM",t,null);n=o.nextNode();){if(vt("uponSanitizeShadowNode",n,null),Lt(n))continue;const t=ie(n);n.nodeType===Z&&(t&&t.__depth?n.__depth=(n.__removalCount||0)+t.__depth+1:n.__depth=1),(n.__depth>=pt||n.__depth<0||b(n.__depth))&&Nt(n),n.content instanceof s&&(n.content.__depth=n.__depth,e(n.content)),xt(n)}vt("afterSanitizeShadowDOM",t,null)};return o.sanitize=function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=null,r=null,i=null,l=null;if(rt=!e,rt&&(e="\x3c!--\x3e"),"string"!=typeof e&&!Ct(e)){if("function"!=typeof e.toString)throw A("toString is not a function");if("string"!=typeof(e=e.toString()))throw A("dirty is not a string, aborting")}if(!o.isSupported)return e;if(Pe||ht(t),o.removed=[],"string"==typeof e&&(Xe=!1),Xe){if(e.nodeName){const t=ut(e.nodeName);if(!be[t]||ve[t])throw A("root node is forbidden and cannot be sanitized in-place")}}else if(e instanceof S)n=St("\x3c!----\x3e"),r=n.ownerDocument.importNode(e,!0),r.nodeType===Z&&"BODY"===r.nodeName||"HTML"===r.nodeName?n=r:n.appendChild(r);else{if(!He&&!Me&&!Ue&&-1===e.indexOf("<"))return ae&&Be?ae.createHTML(e):e;if(n=St(e),!n)return He?null:Be?le:""}n&&Fe&&Nt(n.firstChild);const c=Rt(Xe?e:n);for(;i=c.nextNode();){if(Lt(i))continue;const e=ie(i);i.nodeType===Z&&(e&&e.__depth?i.__depth=(i.__removalCount||0)+e.__depth+1:i.__depth=1),(i.__depth>=pt||i.__depth<0||b(i.__depth))&&Nt(i),i.content instanceof s&&(i.content.__depth=i.__depth,kt(i.content)),xt(i)}if(Xe)return e;if(He){if(ze)for(l=ue.call(n.ownerDocument);n.firstChild;)l.appendChild(n.firstChild);else l=n;return(Re.shadowroot||Re.shadowrootmode)&&(l=pe.call(a,l,!0)),l}let m=Ue?n.outerHTML:n.innerHTML;return Ue&&be["!doctype"]&&n.ownerDocument&&n.ownerDocument.doctype&&n.ownerDocument.doctype.name&&E($,n.ownerDocument.doctype.name)&&(m="<!DOCTYPE "+n.ownerDocument.doctype.name+">\n"+m),Me&&u([de,he,ge],(e=>{m=g(m,e," ")})),ae&&Be?ae.createHTML(m):m},o.setConfig=function(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};ht(e),Pe=!0},o.clearConfig=function(){mt=null,Pe=!1},o.isValidAttribute=function(e,t,n){mt||ht({});const o=ut(e),r=ut(t);return Dt(o,r,n)},o.addHook=function(e,t){"function"==typeof t&&(fe[e]=fe[e]||[],p(fe[e],t))},o.removeHook=function(e){if(fe[e])return m(fe[e])},o.removeHooks=function(e){fe[e]&&(fe[e]=[])},o.removeAllHooks=function(){fe={}},o}();return re}));
//# sourceMappingURL=purify.min.js.map

// scripts/api.js
const publicToken = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA";
function getCsrf() {
    let csrf = document.cookie.match(/(?:^|;\s*)ct0=([0-9a-f]+)\s*(?:;|$)/);
    return csrf ? csrf[1] : "";
}

function debugLog(...args) {
    if(typeof vars === "object" && vars.developerMode) {
        if(args[0] === 'notifications.get' && !document.querySelector('.notifications-modal') && !location.pathname.startsWith('/notifications')) return; 
        if(vars.extensiveLogging) {
            console.trace(...args);
        } else {
            console.log(...args, new Error().stack.split("\n")[2].trim()); // genius
        }
    }
}

// extract full text and url entities from "note_tweet"
function parseNoteTweet(result) {
    let text, entities;
    if(result.note_tweet.note_tweet_results.result) {
        text = result.note_tweet.note_tweet_results.result.text;
        entities = result.note_tweet.note_tweet_results.result.entity_set;
        if(result.note_tweet.note_tweet_results.result.richtext?.richtext_tags.length) {
            entities.richtext = result.note_tweet.note_tweet_results.result.richtext.richtext_tags // logically, richtext is an entity, right?
        }
    } else {
        text = result.note_tweet.note_tweet_results.text;
        entities = result.note_tweet.note_tweet_results.entity_set;
    }
    return {text, entities};
}


// transform ugly useless twitter api reply to usable legacy tweet
function parseTweet(res) {
    if(typeof res !== "object") return;
    if(res.limitedActionResults) {
        let limitation = res.limitedActionResults.limited_actions.find(l => l.action === "Reply");
        if(limitation) {
            res.tweet.legacy.limited_actions_text = limitation.prompt ? limitation.prompt.subtext.text : LOC.limited_tweet.message;
        }
        res = res.tweet;
    }
    if(!res.legacy && res.tweet) res = res.tweet;
    let tweet = res.legacy;
    if(!res.core) return;
    tweet.user = res.core.user_results.result.legacy;
    tweet.user.id_str = tweet.user_id_str;
    if(res.core.user_results.result.is_blue_verified && !res.core.user_results.result.legacy.verified_type) {
        tweet.user.verified = true;
        tweet.user.verified_type = "Blue";
    }
    if(tweet.retweeted_status_result) {
        let result = tweet.retweeted_status_result.result;
        if(result.limitedActionResults && result.tweet && result.tweet.legacy) {
            let limitation = result.limitedActionResults.limited_actions.find(l => l.action === "Reply");
            if(limitation) {
                result.tweet.legacy.limited_actions_text = limitation.prompt ? limitation.prompt.subtext.text : LOC.limited_tweet.message;
            }
        }
        if(result.tweet) result = result.tweet;
        if(
            result.quoted_status_result && 
            result.quoted_status_result.result && 
            result.quoted_status_result.result.legacy &&
            result.quoted_status_result.result.core &&
            result.quoted_status_result.result.core.user_results.result.legacy    
        ) {
            result.legacy.quoted_status = result.quoted_status_result.result.legacy;
            if(result.legacy.quoted_status) {
                result.legacy.quoted_status.user = result.quoted_status_result.result.core.user_results.result.legacy;
                result.legacy.quoted_status.user.id_str = result.legacy.quoted_status.user_id_str;
                if(result.quoted_status_result.result.core.user_results.result.is_blue_verified && !result.quoted_status_result.result.core.user_results.result.legacy.verified_type) {
                    result.legacy.quoted_status.user.verified = true;
                    result.legacy.quoted_status.user.verified_type = "Blue";
                }
                tweetStorage[result.legacy.quoted_status.id_str] = result.legacy.quoted_status;
                tweetStorage[result.legacy.quoted_status.id_str].cacheDate = Date.now();
                userStorage[result.legacy.quoted_status.user.id_str] = result.legacy.quoted_status.user;
                userStorage[result.legacy.quoted_status.user.id_str].cacheDate = Date.now();
            } else {
                console.warn("No retweeted quoted status", result);
            }
        } else if(
            result.quoted_status_result &&
            result.quoted_status_result.result &&  
            result.quoted_status_result.result.tweet && 
            result.quoted_status_result.result.tweet.legacy &&
            result.quoted_status_result.result.tweet.core &&
            result.quoted_status_result.result.tweet.core.user_results.result.legacy    
        ) {
            result.legacy.quoted_status = result.quoted_status_result.result.tweet.legacy;
            if(result.legacy.quoted_status) {
                result.legacy.quoted_status.user = result.quoted_status_result.result.tweet.core.user_results.result.legacy;
                result.legacy.quoted_status.user.id_str = result.legacy.quoted_status.user_id_str;
                if(result.quoted_status_result.result.tweet.core.user_results.result.is_blue_verified && !result.core.user_results.result.verified_type) {
                    result.legacy.quoted_status.user.verified = true;
                    result.legacy.quoted_status.user.verified_type = "Blue";
                }
                tweetStorage[result.legacy.quoted_status.id_str] = result.legacy.quoted_status;
                tweetStorage[result.legacy.quoted_status.id_str].cacheDate = Date.now();
                userStorage[result.legacy.quoted_status.user.id_str] = result.legacy.quoted_status.user;
                userStorage[result.legacy.quoted_status.user.id_str].cacheDate = Date.now();
            } else {
                console.warn("No retweeted quoted status", result);
            }
        }
        tweet.retweeted_status = result.legacy;
        if(tweet.retweeted_status && result.core.user_results.result.legacy) {
            tweet.retweeted_status.user = result.core.user_results.result.legacy;
            tweet.retweeted_status.user.id_str = tweet.retweeted_status.user_id_str;
            if(result.core.user_results.result.is_blue_verified && !result.core.user_results.result.legacy.verified_type) {
                tweet.retweeted_status.user.verified = true;
                tweet.retweeted_status.user.verified_type = "Blue";
            }
            tweet.retweeted_status.ext = {};
            if(result.views) {
                tweet.retweeted_status.ext.views = {r: {ok: {count: +result.views.count}}};
            }
            tweet.retweeted_status.res = res;
            if(res.card && res.card.legacy && res.card.legacy.binding_values) {
                tweet.retweeted_status.card = res.card.legacy;
            }
            tweetStorage[tweet.retweeted_status.id_str] = tweet.retweeted_status;
            tweetStorage[tweet.retweeted_status.id_str].cacheDate = Date.now();
            userStorage[tweet.retweeted_status.user.id_str] = tweet.retweeted_status.user;
            userStorage[tweet.retweeted_status.user.id_str].cacheDate = Date.now();
        } else {
            console.warn("No retweeted status", result);
        }
        if(result.note_tweet && result.note_tweet.note_tweet_results) {
            let note = parseNoteTweet(result);
            tweet.retweeted_status.full_text = note.text;
            tweet.retweeted_status.entities = note.entities;
            tweet.retweeted_status.display_text_range = undefined; // no text range for long tweets
        }
    }

    if(res.quoted_status_result) {
        tweet.quoted_status_result = res.quoted_status_result;
    }
    if(res.note_tweet && res.note_tweet.note_tweet_results) {
        let note = parseNoteTweet(res);
        tweet.full_text = note.text;
        tweet.entities = note.entities;
        tweet.display_text_range = undefined; // no text range for long tweets
    }
    if(tweet.quoted_status_result && tweet.quoted_status_result.result) {
        let result = tweet.quoted_status_result.result;
        if(!result.core && result.tweet) result = result.tweet;
        if(result.limitedActionResults) {
            let limitation = result.limitedActionResults.limited_actions.find(l => l.action === "Reply");
            if(limitation) {
                result.tweet.legacy.limited_actions_text = limitation.prompt ? limitation.prompt.subtext.text : LOC.limited_tweet.message;
            }
            result = result.tweet;
        }
        tweet.quoted_status = result.legacy;
        if(tweet.quoted_status) {
            tweet.quoted_status.user = result.core.user_results.result.legacy;
            if(!tweet.quoted_status.user) {
                delete tweet.quoted_status;
            } else {
                tweet.quoted_status.user.id_str = tweet.quoted_status.user_id_str;
                if(result.core.user_results.result.is_blue_verified && !result.core.user_results.result.legacy.verified_type) {
                    tweet.quoted_status.user.verified = true;
                    tweet.quoted_status.user.verified_type = "Blue";
                }
                tweet.quoted_status.ext = {};
                if(result.views) {
                    tweet.quoted_status.ext.views = {r: {ok: {count: +result.views.count}}};
                }
                tweetStorage[tweet.quoted_status.id_str] = tweet.quoted_status;
                tweetStorage[tweet.quoted_status.id_str].cacheDate = Date.now();
                userStorage[tweet.quoted_status.user.id_str] = tweet.quoted_status.user;
                userStorage[tweet.quoted_status.user.id_str].cacheDate = Date.now();
            }
        } else {
            console.warn("No quoted status", result);
        }
    }
    if(res.card && res.card.legacy) {
        tweet.card = res.card.legacy;
        let bvo = {};
        for(let i = 0; i < tweet.card.binding_values.length; i++) {
            let bv = tweet.card.binding_values[i];
            bvo[bv.key] = bv.value;
        }
        tweet.card.binding_values = bvo;
    }
    if(res.views) {
        if(!tweet.ext) tweet.ext = {};
        tweet.ext.views = {r: {ok: {count: +res.views.count}}};
    }
    if(res.source) {
        tweet.source = res.source;
    }
    if(res.birdwatch_pivot) { // community notes
        tweet.birdwatch = res.birdwatch_pivot;
    }
    if(res.trusted_friends_info_result && res.trusted_friends_info_result.owner_results && res.trusted_friends_info_result.owner_results.result && res.trusted_friends_info_result.owner_results.result.legacy) {
        tweet.trusted_circle_owner = res.trusted_friends_info_result.owner_results.result.legacy.screen_name;
    }

    if(tweet.favorited && tweet.favorite_count === 0) {
        tweet.favorite_count = 1;
    }
    if(tweet.retweeted && tweet.retweet_count === 0) {
        tweet.retweet_count = 1;
    }

    tweet.res = res;

    tweetStorage[tweet.id_str] = tweet;
    tweetStorage[tweet.id_str].cacheDate = Date.now();
    userStorage[tweet.user.id_str] = tweet.user;
    userStorage[tweet.user.id_str].cacheDate = Date.now();
    return tweet;
}


const API = {
    account: {
        verifyCredentials: () => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/account/verify_credentials.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session"
                    },
                    credentials: "include"
                }).then(response => response.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
    },
    user: {
        get: (val, byId = true) => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/users/show.json?${byId ? `user_id=${val}` : `screen_name=${val}`}`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "x-twitter-client-language": window.LANGUAGE ? window.LANGUAGE : navigator.language ? navigator.language : "en"
                    },
                    credentials: "include"
                }).then(i => {
                    if(i.status === 401) {
                        setTimeout(() => {
                            location.href = `/i/flow/login?newtwitter=true`;
                        }, 50);
                    }
                    return i.json();
                }).then(data => {
                    debugLog('user.get', {val, byId, data});
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        getV2: name => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/sLVLhk0bGj3MVFEKTdax1w/UserByScreenName?variables=%7B%22screen_name%22%3A%22${name}%22%2C%22withSafetyModeUserFields%22%3Atrue%2C%22withSuperFollowsUserFields%22%3Atrue%7D&features=${encodeURIComponent(JSON.stringify({"blue_business_profile_image_shape_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true}))}`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json",
                        "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    debugLog('user.getV2', 'start', {name, data});
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    if(data.data.user.result.unavailable_message) {
                        return reject(data.data.user.result.unavailable_message.text);
                    }

                    let result = data.data.user.result;
                    result.legacy.id_str = result.rest_id;
                    if(result.legacy_extended_profile.birthdate) {
                        result.legacy.birthdate = result.legacy_extended_profile.birthdate;
                    }
                    if(result.professional) {
                        result.legacy.professional = result.professional;
                    }
                    if(result.affiliates_highlighted_label && result.affiliates_highlighted_label.label) {
                        result.legacy.affiliates_highlighted_label = result.affiliates_highlighted_label.label;
                    }
                    if(result.is_blue_verified && !result.legacy.verified_type) {
                        result.legacy.verified_type = "Blue";
                    }
        
                    debugLog('user.getV2', 'end', result.legacy);
                    resolve(result.legacy);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        follow: screen_name => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/friendships/create.json`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    credentials: "include",
                    body: `screen_name=${screen_name}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unfollow: screen_name => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/friendships/destroy.json`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    credentials: "include",
                    body: `screen_name=${screen_name}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        receiveNotifications: (id, receive = false) => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/friendships/update.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_has_nft_avatar=1&skip_status=1&cursor=-1&id=${id}&device=${receive}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        block: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/blocks/create.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `user_id=${id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unblock: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/blocks/destroy.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `user_id=${id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }

                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        mute: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/mutes/users/create.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `user_id=${id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unmute: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/mutes/users/destroy.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `user_id=${id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }

                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        removeFollower: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/QpNfg0kpPRfjROQ_9eOLXA/RemoveFollower`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json"
                    },
                    credentials: "include",
                    method: 'post',
                    body: JSON.stringify({"variables":{"target_user_id":id},"queryId":"QpNfg0kpPRfjROQ_9eOLXA"})
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        lookup: ids => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/users/lookup.json?user_id=${ids.join(",")}`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        getFollowersYouFollow: (id, cursor) => {
            return new Promise((resolve, reject) => {
                let obj = {
                    "userId": id,
                    "count": 50,
                    "includePromotedContent": false
                };
                if(cursor) obj.cursor = cursor;
                GM_fetch(`/i/api/graphql/m8AXvuS9H0aAI09J3ISOrw/FollowersYouKnow?variables=${encodeURIComponent(JSON.stringify(obj))}&features=${encodeURIComponent(JSON.stringify({"rweb_lists_timeline_redesign_enabled":false,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_enhance_cards_enabled":false}))}`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    debugLog('user.getFollowersYouFollow', 'start', {id, cursor, data});
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    let list = data.data.user.result.timeline.timeline.instructions.find(i => i.type === 'TimelineAddEntries').entries;
                    const out = {
                        list: list.filter(e => e.entryId.startsWith('user-')).map(e => {
                            let user = e.content.itemContent.user_results.result;
                            user.legacy.id_str = user.rest_id;
                            if(user.is_blue_verified && !user.legacy.verified_type) {
                                user.legacy.verified = true;
                                user.legacy.verified_type = "Blue";
                            }
                            return user.legacy;
                        }),
                        cursor: list.find(e => e.entryId.startsWith('cursor-bottom-')).content.value
                    };
                    debugLog('user.getFollowersYouFollow', 'end', out);
                    resolve(out);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        switchRetweetsVisibility: (user_id, see) => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/friendships/update.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `id=${user_id}&retweets=${see}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        getFollowRequests: (cursor = -1) => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/friendships/incoming.json?include_profile_interstitial_type=1&include_blocking=1&include_blocked_by=1&include_followed_by=1&include_want_retweets=1&include_mute_edge=1&include_can_dm=1&include_can_media_tag=1&include_ext_has_nft_avatar=1&skip_status=1&cursor=${cursor}&stringify_ids=true&count=100`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        acceptFollowRequest: user_id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/friendships/accept.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `user_id=${user_id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        declineFollowRequest: user_id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/friendships/deny.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `user_id=${user_id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
    },
    tweet: {
        post: data => { // deprecated
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/statuses/update.json`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    body: new URLSearchParams(data).toString(),
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        /* 
            text | tweet_text | status - tweet text
            media | media_ids - media ids
            card_uri - card uri
            sensitive - sensitive media
            in_reply_to_status_id | in_reply_to_tweet_id - reply to tweet id
            exclude_reply_user_ids - exclude mentions
            attachment_url - quote tweet url
            circle - circle id
            conversation_control - conversation control (follows | mentions)
        */
        postV2: tweet => {
            return new Promise((resolve, reject) => {
                let text;
                if(tweet.text) {
                    text = tweet.text;
                } else if(tweet.tweet_text) {
                    text = tweet.tweet_text;
                } else if(tweet.status) {
                    text = tweet.status;
                } else {
                    text = "";
                }
                let variables = {
                    "tweet_text": text,
                    "media": {
                        "media_entities": [],
                        "possibly_sensitive": false
                    },
                    "semantic_annotation_ids": [],
                    "dark_request": false
                };
                if(tweet.card_uri) {
                    variables.card_uri = tweet.card_uri;
                }
                if(tweet.media_ids) {
                    if(typeof tweet.media_ids === "string") {
                        tweet.media = tweet.media_ids.split(",");
                    } else {
                        tweet.media = tweet.media_ids;
                    }
                }
                if(tweet.media) {
                    variables.media.media_entities = tweet.media.map(i => ({media_id: i, tagged_users: []}));
                    if(tweet.sensitive) {
                        variables.media.possibly_sensitive = true;
                    }
                }
                if(tweet.conversation_control === 'follows') {
                    variables.conversation_control = { mode: 'Community' };
                } else if(tweet.conversation_control === 'mentions') {
                    variables.conversation_control = { mode: 'ByInvitation' };
                }
                if(tweet.circle) {
                    variables.trusted_friends_control_options = { "trusted_friends_list_id": tweet.circle };
                }
                if(tweet.in_reply_to_status_id) {
                    tweet.in_reply_to_tweet_id = tweet.in_reply_to_status_id;
                    delete tweet.in_reply_to_status_id;
                }
                if(tweet.in_reply_to_tweet_id) {
                    variables.reply = {
                        in_reply_to_tweet_id: tweet.in_reply_to_tweet_id,
                        exclude_reply_user_ids: []
                    }
                    if(tweet.exclude_reply_user_ids) {
                        if(typeof tweet.exclude_reply_user_ids === "string") {
                            tweet.exclude_reply_user_ids = tweet.exclude_reply_user_ids.split(",");
                        }
                        variables.reply.exclude_reply_user_ids = tweet.exclude_reply_user_ids;
                    }
                }
                if(tweet.attachment_url) {
                    variables.attachment_url = tweet.attachment_url;
                }
                debugLog('tweet.postV2', 'init', {tweet, variables});
                let parsedTweet = twttr.txt.parseTweet(text);
                GM_fetch(`/i/api/graphql/${parsedTweet.weightedLength > 280 ? 'cuvrhmg0s4pGaLWV68NNnQ/CreateNoteTweet' : 'I_J3_LvnnihD0Gjbq5pD2g/CreateTweet'}`, {
                    method: 'POST',
                    headers: {
                        "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw",
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8",
                        "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
                    },
                    credentials: "include",
                    body: JSON.stringify({
                        variables,
                        "features": {"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"responsive_web_home_pinned_timelines_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false},
                        "queryId": parsedTweet.weightedLength > 280 ? 'cuvrhmg0s4pGaLWV68NNnQ' : 'I_J3_LvnnihD0Gjbq5pD2g'
                    })
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.postV2', 'start', data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    let ct = data.data.create_tweet ? data.data.create_tweet : data.data.notetweet_create;
                    let result = ct.tweet_results.result;
                    let tweet = parseTweet(result);
                    if(result.trusted_friends_info_result && !tweet.limited_actions) {
                        tweet.limited_actions = 'limit_trusted_friends_tweet';
                    }
                    debugLog('tweet.postV2', 'end', tweet);
                    resolve(tweet);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        favorite: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/lI07N6Otwv1PhnEgXILM7A/FavoriteTweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"tweet_id":id},"queryId":"lI07N6Otwv1PhnEgXILM7A"})
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unfavorite: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/ZYKSe-w7KEslx3JhSIk5LA/UnfavoriteTweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"tweet_id":id},"queryId":"ZYKSe-w7KEslx3JhSIk5LA"})
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        retweet: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/ojPdsZsimiJrUGLR1sjUtA/CreateRetweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"tweet_id":id,"dark_request":false},"queryId":"ojPdsZsimiJrUGLR1sjUtA"})
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.retweet', id, data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unretweet: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/iQtK4dl5hBmXewYZuEOKVw/DeleteRetweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"source_tweet_id":id,"dark_request":false},"queryId":"iQtK4dl5hBmXewYZuEOKVw"})
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.unretweet', id, data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        delete: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"tweet_id":id,"dark_request":false},"queryId":"VaenaVgh5q5ih7kvyVjgtg"})
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.delete', id, data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        get: id => { // deprecated
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/statuses/show.json?id=${id}&include_my_retweet=1&cards_platform=Web13&include_entities=1&include_user_entities=1&include_cards=1&send_error_codes=1&tweet_mode=extended&include_ext_alt_text=true&include_reply_count=true&ext=views%2CmediaStats%2CverifiedType%2CisBlueVerified`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        vote: (api, tweet_id, card_uri, card_name, selected_choice) => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://caps.${location.hostname}/v2/capi/${api.split('//')[1]}`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `twitter%3Astring%3Acard_uri=${encodeURIComponent(card_uri)}&twitter%3Along%3Aoriginal_tweet_id=${tweet_id}&twitter%3Astring%3Aresponse_card_name=${card_name}&twitter%3Astring%3Acards_platform=Web-12&twitter%3Astring%3Aselected_choice=${selected_choice}`
                }).then(response => response.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            })
        },
        createCard: card_data => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://caps.${location.hostname}/v2/cards/create.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `card_data=${encodeURIComponent(JSON.stringify(card_data))}`
                }).then(response => response.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            })
        },
        mute: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/mutes/conversations/create.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `tweet_id=${id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unmute: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/mutes/conversations/destroy.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `tweet_id=${id}`
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        lookup: ids => {
            return new Promise((resolve, reject) => {
                GM_fetch(`https://api.${location.hostname}/1.1/statuses/lookup.json?id=${ids.join(',')}&include_entities=true&include_ext_alt_text=true&include_card_uri=true&tweet_mode=extended&include_reply_count=true&ext=views%2CmediaStats`, {
                    headers: {
                        "authorization": "Bearer AAAAAAAAAAAAAAAAAAAAAFQODgEAAAAAVHTp76lzh3rFzcHbmHVvQxYYpTw%3DckAlMINMjmCwxUcaXbAN4XqJVdgMJaHqNOFgPMK0zN1qLqLQCF",
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "x-twitter-client-language": navigator.language ? navigator.language : "en"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    if (data.errors && data.errors[0].code === 32) {
                        return reject("Not logged in");
                    }
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        pin: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/account/pin_tweet.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `id=${id}`
                }).then(i => i.text()).then(data => {
                    resolve(true);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unpin: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/1.1/account/unpin_tweet.json`, {
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    credentials: "include",
                    method: 'post',
                    body: `id=${id}`
                }).then(i => i.text()).then(data => {
                    resolve(true);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        moderate: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/pjFnHGVqCjTcZol0xcBJjw/ModerateTweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"tweetId":id},"queryId":"pjFnHGVqCjTcZol0xcBJjw"})
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.moderate', id, data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        unmoderate: id => {
            return new Promise((resolve, reject) => {
                GM_fetch(`/i/api/graphql/pVSyu6PA57TLvIE4nN2tsA/UnmoderateTweet`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/json; charset=utf-8"
                    },
                    credentials: "include",
                    body: JSON.stringify({"variables":{"tweetId":"1683331680751308802"},"queryId":"pVSyu6PA57TLvIE4nN2tsA"})
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.unmoderate', id, data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        },
        getModeratedReplies: (id, cursor) => {
            return new Promise((resolve, reject) => {
                let variables = {"rootTweetId":id,"count":20,"includePromotedContent":false};
                if(cursor) variables.cursor = cursor;
                GM_fetch(`/i/api/graphql/SiKS1_3937rb72ytFnDHmA/ModeratedTimeline?variables=${encodeURIComponent(JSON.stringify(variables))}&features=${encodeURIComponent(JSON.stringify({"rweb_lists_timeline_redesign_enabled":false,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":false,"tweet_awards_web_tipping_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_enhance_cards_enabled":false}))}`, {
                    method: 'POST',
                    headers: {
                        "authorization": publicToken,
                        "x-csrf-token": getCsrf(),
                        "x-twitter-auth-type": "OAuth2Session",
                        "content-type": "application/x-www-form-urlencoded",
                        "x-twitter-client-language": LANGUAGE ? LANGUAGE : navigator.language ? navigator.language : "en"
                    },
                    credentials: "include"
                }).then(i => i.json()).then(data => {
                    debugLog('tweet.getModeratedReplies', 'start', id, data);
                    if (data.errors && data.errors[0]) {
                        return reject(data.errors[0].message);
                    }
                    let entries = data.data.tweet.result.timeline_response.timeline.instructions.find(i => i.entries);
                    if(!entries) return resolve({
                        list: [],
                        cursor: undefined
                    });
                    entries = entries.entries;
                    let list = entries.filter(e => e.entryId.startsWith('tweet-'));
                    let cursor = entries.find(e => e.entryId.startsWith('cursor-bottom'));
                    if(!cursor) {
                        let entries = data.data.tweet.result.timeline_response.timeline.instructions.find(i => i.replaceEntry && i.replaceEntry.entryIdToReplace.includes('cursor-bottom'));
                        if(entries) {
                            cursor = entries.replaceEntry.entry.content.operation.cursor.value;
                        }
                    } else {
                        cursor = cursor.content.operation.cursor.value;
                    }
                    let out = {
                        list: list.map(e => {
                            let tweet = parseTweet(e.content.itemContent.tweet_results.result);
                            if(!tweet) return;
                            tweet.moderated = true;
                            return tweet;
                        }).filter(e => e),
                        cursor
                    };
                    debugLog('tweet.getModeratedReplies', 'end', id, out);
                    resolve(data);
                }).catch(e => {
                    reject(e);
                });
            });
        }
    },
};

// scripts/helpers.js
function createModal(html, className, onclose, canclose) {
    let modal = document.createElement('div');
    modal.classList.add('yeah-modal');
    let modal_content = document.createElement('div');
    modal_content.classList.add('yeah-modal-content');
    if(className) modal_content.classList.add(className);
    modal_content.innerHTML = html;
    modal.appendChild(modal_content);
    let close = document.createElement('span');
    close.classList.add('yeah-modal-close');
    close.title = "ESC";
    close.innerHTML = '&times;';
    document.body.style.overflowY = 'hidden';
    function removeModal() {
        modal.remove();
        let event = new Event('findActiveTweet');
        document.dispatchEvent(event);
        document.removeEventListener('keydown', escapeEvent);
        if(onclose) onclose();
        let modals = document.getElementsByClassName('modal');
        if(modals.length === 0) {
            document.body.style.overflowY = '';
        }
    }
    modal.removeModal = removeModal;
    function escapeEvent(e) {
        if(document.querySelector('.viewer-in')) return;
        if(e.key === 'Escape' || (e.altKey && e.keyCode === 78)) {
            if(!canclose || canclose()) removeModal();
        }
    }
    close.addEventListener('click', removeModal);
    let isHoldingMouseFromContent = false;
    modal_content.addEventListener('mousedown', () => {
        isHoldingMouseFromContent = true;
    });
    document.addEventListener('mouseup', () => {
        setTimeout(() => isHoldingMouseFromContent = false, 10);
    });
    modal.addEventListener('click', e => {
        if(e.target === modal && !isHoldingMouseFromContent) {
            if(!canclose || canclose()) removeModal();
        }
    });
    document.addEventListener('keydown', escapeEvent);
    modal_content.appendChild(close);
    document.body.appendChild(modal);
    return modal;
}

async function callTwitterApi(method = 'GET', path, headers = {}, body) {
    if(typeof body === 'object' && !headers['Content-Type']) {
        body = JSON.stringify(body);
        headers['Content-Type'] = 'application/json';
    }
    if(headers['Content-Type'] === 'application/x-www-form-urlencoded') {
        body = new URLSearchParams(body).toString();
    }
    if(!headers['Authorization']) {
        headers['Authorization'] = `Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA`;
    }
    if(!headers['x-csrf-token']) {
        let csrf = document.cookie.match(/(?:^|;\s*)ct0=([0-9a-f]+)\s*(?:;|$)/);
        headers['x-csrf-token'] = csrf ? csrf[1] : "";
    }
    headers['x-twitter-auth-type'] = 'OAuth2Session';
    headers['x-twitter-active-user'] = 'yes';
    headers['x-twitter-client-language'] = 'en';

    let res = await GM_fetch(`https://${location.hostname}/i/api${path}`, {
        method,
        headers,
        body
    }).then(res => res.json());

    if(res.errors) {
        throw new Error(res.errors[0].message);
    }

    return res;
};

async function callYeahApi(path, body = {}) {
    let token = await getYeahToken();
    if(token) body.key = token;

    const res = await GM_fetch(API_URL + path, {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(body)
    });
    let result = await res.text();

    if(result === 'Invalid key') {
        chrome.storage.local.remove('yeahToken');
        chrome.storage.local.get('yeahTokens', async result => {
            if(result.yeahTokens) {
                let userId = await getUserId();
                delete result.yeahTokens[userId];
                chrome.storage.local.set(result);
            }
        });
        throw new Error('Invalid key');
    }

    return result;
}

let _userId;
async function getUserId() {
    if(!_userId) {
        let user = await API.account.verifyCredentials();
        _userId = user.id_str;
    }
    return _userId;
}

function getYeahToken() {
    return new Promise(async (resolve, reject) => {
        chrome.storage.local.get(['yeahToken', 'yeahTokens'], async result => {
            if(result) {
                let userId = await getUserId();
                if(result.yeahTokens && result.yeahTokens[userId]) {
                    resolve(result.yeahTokens[userId]);
                } else {
                    resolve(result.yeahToken);
                }
            } else {
                resolve(null);
            } 
        });
    });
}

function getYeahSettings() {
    return new Promise((resolve, reject) => {
        chrome.storage.local.get('settings', result => {
            if(result && result.settings) {
                resolve(result.settings);
            } else {
                resolve({});
            } 
        });
    });
}

function formatLargeNumber(n) {
    let option = {notation: 'compact', compactDisplay: 'short', maximumFractionDigits: 1, minimumFractionDigits: 1};
    if (n >= 1e3) {
        return Number(n).toLocaleString('en-US', option);
    }
    else return Number(n).toLocaleString();
}

function escapeHTML(unsafe) {
    if(typeof unsafe === 'undefined' || unsafe === null) {
        return '';
    }
    return DOMPurify.sanitize(String(unsafe));
}

async function appendUser(u, container, label) {
    let userElement = document.createElement('div');
    userElement.classList.add('user-item');

    userElement.innerHTML = /*html*/`
        <div>
            <a href="/${u.screen_name}" class="user-item-link" target="_blank">
                <img src="${u.profile_image_url_https}" alt="${u.screen_name}" class="user-item-avatar tweet-avatar" width="48" height="48">
                <div class="user-item-text">
                    <span class="yeah-name user-item-name${u.protected ? ' user-protected' : ''}${u.muting ? ' user-muted' : ''}${u.verified || u.verified_type ? ' user-verified' : u.id_str === '1708130407663759360' ? ' user-verified user-verified-dimden' : ''} ${u.verified_type === 'Government' ? 'user-verified-gray' : u.verified_type === 'Business' ? 'user-verified-yellow' : u.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(u.name)}</span><br>
                    <span class="yeah-handle">@${u.screen_name}</span>
                    ${u.followed_by ? `<span class="follows-you-label">Follows you</span>` : ''}
                    ${label ? `<br><span class="user-item-additional">${escapeHTML(label)}</span>` : ''}
                </div>
            </a>
        </div>
        <button class="user-yeah-item-btn nice-yeah-button ${u.following ? 'yeah-following' : 'yeah-follow'}">${u.following ? "Following" : "Follow"}</button>
    `;

    let followButton = userElement.querySelector('.user-yeah-item-btn');
    followButton.addEventListener('click', async () => {
        if (followButton.classList.contains('yeah-following')) {
            try {
                await API.user.unfollow(u.screen_name);
            } catch(e) {
                console.error(e);
                alert(e);
                return;
            }
            followButton.classList.remove('yeah-following');
            followButton.classList.add('yeah-follow');
            followButton.innerText = "Follow";
        } else {
            try {
                await API.user.follow(u.screen_name);
            } catch(e) {
                console.error(e);
                alert(e);
                return;
            }
            followButton.classList.remove('yeah-follow');
            followButton.classList.add('yeah-following');
            followButton.innerText = "Following";
        }
    });

    container.appendChild(userElement);
}


// scripts/tweetrenderer.js

let lastTweetErrorDate = 0;
const mediaClasses = [
    undefined,
    'tweet-media-element-one',
    'tweet-media-element-two',
    'tweet-media-element-three',
    'tweet-media-element-two',
];

function calculateSize(x, y, max_x, max_y) {
    let ratio = x / y;
    let iw = innerWidth;
    if(iw < 590) max_x = iw - 120;
    if(x > max_x) {
        x = max_x;
        y = x / ratio;
    }
    if(y > max_y) {
        y = max_y;
        x = y * ratio;
    }
    return [parseInt(x), parseInt(y)];
}

const sizeFunctions = [
    undefined,
    (w, h) => calculateSize(w, h, 450, 500),
    (w, h) => calculateSize(w, h, 225, 400),
    (w, h) => innerWidth < 590 ? calculateSize(w, h, 225, 400) : calculateSize(w, h, 150, 250),
    (w, h) => calculateSize(w, h, 225, 400),
    (w, h) => calculateSize(w, h, 225, 400),
    (w, h) => calculateSize(w, h, 225, 400),
    (w, h) => calculateSize(w, h, 225, 400),
    (w, h) => calculateSize(w, h, 225, 400)
];

const quoteSizeFunctions = [
    undefined,
    (w, h) => calculateSize(w, h, 400, 400),
    (w, h) => calculateSize(w, h, 200, 400),
    (w, h) => calculateSize(w, h, 125, 200),
    (w, h) => calculateSize(w, h, 100, 150),
    (w, h) => calculateSize(w, h, 100, 150),
    (w, h) => calculateSize(w, h, 100, 150),
    (w, h) => calculateSize(w, h, 100, 150),
    (w, h) => calculateSize(w, h, 100, 150)
];

function html(strings, ...values) {
    let str = '';
    strings.forEach((string, i) => {
        str += string + escapeHTML(values[i]);
    });
    return str;
}

async function handleFiles(files, mediaArray, mediaContainer, is_dm = false) {
    let images = [];
    let videos = [];
    let gifs = [];
    for (let i = 0; i < files.length; i++) {
        let file = files[i];
        if (file.type.includes('gif')) {
            // max 15 mb
            if (file.size > 15000000) {
                return alert("Gifs max size is 15mb");
            }
            gifs.push(file);
        } else if (file.type.includes('video')) {
            // max 500 mb
            if (file.size > 500000000) {
                return alert("Videos max size is 500mb");
            }
            videos.push(file);
        } else if (file.type.includes('image')) {
            // max 5 mb
            if (
                file.size > 5000000 ||
                (window.navigator && navigator.connection && navigator.connection.type === 'cellular')
            ) {
                // convert png to jpeg
                let toBreak = false, i = 0;
                while(file.size > 5000000) {
                    await new Promise(resolve => {
                        let canvas = document.createElement('canvas');
                        let ctx = canvas.getContext('2d');
                        let img = new Image();
                        img.onload = function () {
                            canvas.width = img.width;
                            canvas.height = img.height;
                            ctx.drawImage(img, 0, 0);
                            let dataURL = canvas.toDataURL('image/jpeg', (window.navigator && navigator.connection && navigator.connection.type === 'cellular') ? (0.5 - i*0.1) : (0.9 - i*0.1));
                            let blobBin = atob(dataURL.split(',')[1]);
                            let array = [];
                            for (let i = 0; i < blobBin.length; i++) {
                                array.push(blobBin.charCodeAt(i));
                            }
                            let newFile = new Blob([new Uint8Array(array)], { type: 'image/jpeg' });
                            if(newFile.size > file.size) {
                                toBreak = true;
                            } else {
                                file = newFile;
                            }
                            resolve();
                        };
                        img.src = URL.createObjectURL(file);
                    });
                    if(toBreak || i++ > 5) break;
                }
                if(file.size > 5000000) {
                    return alert("Images max size is 5mb");
                }
            }
            images.push(file);
        }
    }
    // either up to 4 images or 1 video or 1 gif
    if (images.length > 0) {
        if (images.length > 4) {
            images = images.slice(0, 4);
        }
        if (videos.length > 0 || gifs.length > 0) {
            return alert("Images and videos max count is 4");
        }
    }
    if (videos.length > 0) {
        if (images.length > 0 || gifs.length > 0 || videos.length > 1) {
            return alert("Videos max count is 1");
        }
    }
    if (gifs.length > 0) {
        if (images.length > 0 || videos.length > 0 || gifs.length > 1) {
            return alert("Gifs max count is 1");
        }
    }
    // get base64 data
    let media = [...images, ...videos, ...gifs];
    let base64Data = [];
    for (let i = 0; i < media.length; i++) {
        let file = media[i];
        let reader = new FileReader();
        reader.readAsArrayBuffer(file);
        reader.onload = () => {
            base64Data.push(reader.result);
            if (base64Data.length === media.length) {
                while (mediaArray.length >= 4) {
                    mediaArray.pop();
                    mediaContainer.lastChild.remove();
                }
                base64Data.forEach(data => {
                    let div = document.createElement('div');
                    let img = document.createElement('img');
                    div.title = file.name;
                    div.id = `new-tweet-media-img-${Date.now()}${Math.random()}`.replace('.', '-');
                    div.className = "new-tweet-media-img-div";
                    img.className = "new-tweet-media-img";
                    let progress = document.createElement('span');
                    progress.hidden = true;
                    progress.className = "new-tweet-media-img-progress";
                    let remove = document.createElement('span');
                    remove.className = "new-tweet-media-img-remove";
                    let alt;
                    if (!file.type.includes('video')) {
                        alt = document.createElement('span');
                        alt.className = "new-tweet-media-img-alt";
                        alt.innerText = "ALT";
                        alt.addEventListener('click', () => {
                            mediaObject.alt = prompt("Alt text", mediaObject.alt || '');
                        });
                    }
                    let cw = document.createElement('span');
                    cw.className = "new-tweet-media-img-cw";
                    cw.innerText = "CW";
                    cw.addEventListener('click', () => {
                        createModal(`
                            <div class="cw-modal" style="color:var(--almost-black)">
                                <h2 class="nice-header">Content warnings</h2>
                                <br>
                                <input type="checkbox" id="cw-modal-graphic_violence"${mediaObject.cw.includes('graphic_violence') ? ' checked' : ''}> <label for="cw-modal-graphic_violence">Graphic violence</label><br>
                                <input type="checkbox" id="cw-modal-adult_content"${mediaObject.cw.includes('adult_content') ? ' checked' : ''}> <label for="cw-modal-adult_content">Adult content</label><br>
                                <input type="checkbox" id="cw-modal-other"${mediaObject.cw.includes('other') ? ' checked' : ''}> <label for="cw-modal-other">Sensitive content</label><br>
                            </div>
                        `);
                        let graphic_violence = document.getElementById('cw-modal-graphic_violence');
                        let adult_content = document.getElementById('cw-modal-adult_content');
                        let sensitive_content = document.getElementById('cw-modal-other');
                        [graphic_violence, adult_content, sensitive_content].forEach(checkbox => {
                            checkbox.addEventListener('change', () => {
                                if (checkbox.checked) {
                                    mediaObject.cw.push(checkbox.id.slice(9));
                                } else {
                                    let index = mediaObject.cw.indexOf(checkbox.id.slice(9));
                                    if (index > -1) {
                                        mediaObject.cw.splice(index, 1);
                                    }
                                }
                            });
                        });
                    });

                    let mediaObject = {
                        div, img,
                        id: div.id,
                        data: data,
                        type: file.type,
                        cw: [],
                        category: file.type.includes('gif') ? (is_dm ? 'dm_gif' : 'tweet_gif') : file.type.includes('video') ? (is_dm ? 'dm_video' : 'tweet_video') : (is_dm ? 'dm_image' : 'tweet_image')
                    };
                    mediaArray.push(mediaObject);
                    if(file.type.includes('video')) {
                        img.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAWUSURBVHhe7Z1pqG5THMbPNV1jul1TJEOZuqYMRZEpoRARvlw+uIjwASlRFIkMHwzJ8AVfZMhYOGRKESlDkciQyJhknj3PXu9b3nP2sPba9x3Wfp5f/dpr77p1zl7Ped+11l77f5fMz8/PGV3WGByNKA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOA6AOG3eC1gGl4ammXF+h9+HZj0xAdgC3gwPhw5AHjAAL8Kz4Re8UEVTANaCT8HDijOTGy9B9t1fxVkJTWOAneAhoWky5ADIPqykKQCbQA8U84V9xz6spKlzlwyOJl9q+9B/3eI4AOI0zQIOhs+H5iJeh3fBP4qzcjaDF8DNizPTls/gDfCH4qycDeBZcLfibDEcxL8QmotJDQA7fVf4QXFWz8nwvtA0LTkJPhCatewM34LrFGej1AYg9SvgF/hNaDby8eBo2vPp4NjEl5B90hqPAcRxAMRxAMRxAMRxAMRxAMRJDcCaA2NYe3A07Ym9d236Y4TUAGwET4VlCw//Z124MjRNAmfADUOzEnb8iZB90pouS8H/QC5A1C0FMwDcUWTS4YLbz6FZCgOwFaz6Yx7LUrDJh7EsBZue0KcA/Av/Dk0TS18CwIcm/KjbEV4Nf4Qmgr4E4ErIbdAfwUvhXvB+WLkb1gS6BICzAG5Y+KTG2EfGXVn42PRDeAo8AnLjSs5wplV2b4dy3z/7IokuATgHbtfg9vBuOA04JngOHgjPhJ/D3Lgdlt3XhV4Ek0gNAL9jH4RNg66f4J2hOTX4lgx/hj3gdbBuTj1r3At/C81KuA5zD0wa96QGgB0fO+L+c3CcNt/Bi+G+8BGYw4wh9t616Y8R+jIIbMN78AR4NHyTF5RRDADhoInvPO4Pz4NfQUlUAzCE36+3wN0h34D+FUqhHoAhX8Pz4X7wSZg8rcoNB2CUt+Ex8Hj4Li/0HQdgMRxNPwY5W+D8+lvYW1IDsD6Mfc6/zeCYG3zRgq9lcf3gDsj1hEnDRZ4YNoXsk9Z02Q/wDuRKVd3CysbwQrh1cTY+WL7m2dAcG/vAa+ChcFKvzXN2ciPkGKUK7spaBfmJVYbEhpBJBICwZA7HB1dBPnnMAW8IWY3w6SJf1twb3soLueMApMFnHJfBqFJss4wDkE4vyuc4AGlwqzafLLJ4ZtY4AO0Y7sF/A57OC7nTZRYwSyViJjEL4MDvWjjJaaBLxEQyzgBsCS+Hp8FJl8p1iZgpwpU1LmLxxnJL2TTqJLtEzBTg9/yx8DV4PayttJk7DsAo3BfwOHwYruCFvuMABDhYvQm+Co+CMvdFPQB8e/lcyH0A3Bq2HpRCNQD8vY+Er0BuBZOtZKoYgF3gQ/AJuCcvKJMaAI6UaQyzUiJmOeTyLRewjoOxP/80cYmY1QDn7yy1wvk8t3hx5SwXXCImkrKVQC7XchWMu3iqdsvkwFhLxHQZA/Dfcpl02xonVR9o4d65HSCXn5+GOXc+4X6/sns7lNvtkvuxSwBmiSsgV+/4QIQFIvi0juvo3MJlauhLAPhJ9CjkfP4SmPR9qEhfAmAScQDE6RKAWSoR02dcIkYYl4gRxyVixHGJGDNeHABxHABxHABxHABxHABxUgOgUCJmFuAiTwzyJWL6ikvEmM6MbUeQ6QEOgDhNAeB/umDyprYPmwLAKpkydXN7CPuuttJpUwDehy+HpskQDuDZh5U0zQIIN1zeBg+C0yiSYNrDsrbPQL7wyh1FlcQEYAgrYjkAecAARNUwbBMA00M8DRTHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHARDHAZBmbu4/x6swK3hIFr4AAAAASUVORK5CYII=';
                    } else {
                        let dataBase64 = arrayBufferToBase64(data);
                        img.src = `data:${file.type};base64,${dataBase64}`;
                    }
                    remove.addEventListener('click', () => {
                        div.remove();
                        for (let i = mediaArray.length - 1; i >= 0; i--) {
                            let m = mediaArray[i];
                            if (m.id === div.id) mediaArray.splice(i, 1);
                        }
                    });
                    div.append(img, progress, remove);
                    if (!file.type.includes('video')) {
                        img.addEventListener('click', () => {
                            new Viewer(mediaContainer, {
                                transition: false,
                                zoomRatio: 0.3
                            });
                        });
                        div.append(alt);
                    } else {
                        cw.style.marginLeft = '-53px';
                    }
                    div.append(cw);
                    mediaContainer.append(div);
                });
                
                setTimeout(() => {
                    let messageModalElement = document.getElementsByClassName('messages-container')[0];
                    let inboxModalElement = document.getElementsByClassName('inbox-modal')[0];
                    if(messageModalElement) inboxModalElement.scrollTop = inboxModalElement.scrollHeight;
                }, 10);
            }
        }
    }
}
let isURL = (str) => {
    try {
        new URL(str);
        return true;
    } catch (_) {
        return false;
    }
}
function handleDrop(event, mediaArray, mediaContainer) {
    let text = event.dataTransfer.getData("Text").trim();
    if(text.length <= 1) {
        event.stopPropagation();
        event.preventDefault();
        let files = event.dataTransfer.files;
        handleFiles(files, mediaArray, mediaContainer);
    }
}
function getMedia(mediaArray, mediaContainer, is_dm = false) {
    let input = document.createElement('input');
    input.type = 'file';
    input.multiple = true;
    input.accept = 'image/jpeg,image/png,image/webp,image/gif,video/mp4,video/quicktime';
    input.addEventListener('change', () => {
        handleFiles(input.files, mediaArray, mediaContainer, is_dm);
    });
    input.click();
};
function timeElapsed(targetTimestamp) {
    let currentDate = new Date();
    let currentTimeInms = currentDate.getTime();
    let targetDate = new Date(targetTimestamp);
    let targetTimeInms = targetDate.getTime();
    let elapsed = Math.floor((currentTimeInms - targetTimeInms) / 1000);

    if (elapsed < 1) {
        return 'now';
    }
    if (elapsed < 60) { //< 60 sec
        return `${elapsed}s`;
    }
    if (elapsed < 3600) { //< 60 minutes
        return `${Math.floor(elapsed / (60))}m`;
    }
    if (elapsed < 86400) { //< 24 hours
        return `${Math.floor(elapsed / (3600))}h`;
    }
    if (elapsed < 604800) { //<7 days
        return `${Math.floor(elapsed / (86400))}d`;
    }
    if (targetDate.getFullYear() == currentDate.getFullYear()) { // same years
        return targetDate.toLocaleDateString("en-US", { month: 'long', day: 'numeric' });
    }
    //more than last years
    return targetDate.toLocaleDateString("en-US", { year: 'numeric', month: 'long', day: 'numeric' });
}


async function renderTweetBodyHTML(t, is_quoted) {
    let result = "",
        last_pos = 0,
        index_map = {}; // {start_position: [end_position, replacer_func]}
        hashflags = [];

    if(is_quoted) t = t.quoted_status;

    full_text_array = Array.from(t.full_text);

    if (t.entities.richtext) {
        t.entities.richtext.forEach(snippet => {
            //if i felt like it, id write a long-winded series of comments on how much i hate emojis. but i'll refrain
            //and this *still* doesnt work properly with flag emojis
            //im just glad it works at all

            let textBeforeSnippet = t.full_text.slice(0, snippet.from_index);
            let emojisBeforeSnippet = textBeforeSnippet.match(/\p{Extended_Pictographic}/gu);
            emojisBeforeSnippet = emojisBeforeSnippet ? emojisBeforeSnippet.length : 0;

            let fromIndex = snippet.from_index - emojisBeforeSnippet;
            let toIndex = snippet.to_index - emojisBeforeSnippet;

            index_map[fromIndex] = [
                toIndex,
                text => {
                    let snippetText = escapeHTML(full_text_array.slice(fromIndex, toIndex).join(''));
                    let startingTags = `${snippet.richtext_types.includes('Bold') ? '<b>' : ''}${snippet.richtext_types.includes('Italic') ? '<i>' : ''}`;
                    let endingTags = `${snippet.richtext_types.includes('Bold') ? '</b>' : ''}${snippet.richtext_types.includes('Italic') ? '</i>' : ''}`;

                    return `${startingTags}${snippetText}${endingTags}`;
                }
            ];
        });
    }

    if (is_quoted) { // for quoted tweet we need only hashflags and readable urls
        if (t.entities.hashtags) {
            t.entities.hashtags.forEach(hashtag => {
                let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === hashtag.text.toLowerCase());
                index_map[hashtag.indices[0]] = [hashtag.indices[1], text =>
                    `#${escapeHTML(hashtag.text)}`+
                    `${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag">` : ''}`];
            });
        };

        if (t.entities.urls) {
            t.entities.urls.forEach(url => {
                index_map[url.indices[0]] = [url.indices[1], text => `${escapeHTML(url.display_url)}`];
            });
        };
    } else {
        if (t.entities.hashtags) {
            t.entities.hashtags.forEach(hashtag => {
                let hashflag = hashflags.find(h => h.hashtag.toLowerCase() === hashtag.text.toLowerCase());
                index_map[hashtag.indices[0]] = [hashtag.indices[1], text => `<a href="/hashtag/${escapeHTML(hashtag.text)}">`+
                    `#${escapeHTML(hashtag.text)}`+
                    `${hashflag ? `<img src="${hashflag.asset_url}" class="hashflag">` : ''}`+
                `</a>`];
            });
        };

        if (t.entities.symbols) {
            t.entities.symbols.forEach(symbol => {
                index_map[symbol.indices[0]] = [symbol.indices[1], text => `<a href="/search?q=%24${escapeHTML(symbol.text)}">`+
                    `$${escapeHTML(symbol.text)}`+
                `</a>`];
            });
        }

        if (t.entities.urls) {
            t.entities.urls.forEach(url => {
                index_map[url.indices[0]] = [url.indices[1], text =>
                    `<a href="${escapeHTML(url.expanded_url)}" title="${escapeHTML(url.expanded_url)}" target="_blank" rel="noopener noreferrer">`+
                    `${escapeHTML(url.display_url)}</a>`];
            });
        };

        if (t.entities.user_mentions) {
            t.entities.user_mentions.forEach(user => {
                index_map[user.indices[0]] = [user.indices[1], text => `<a href="/${escapeHTML(user.screen_name)}">${escapeHTML(text)}</a>`];
            });
        };

        if(t.entities.media) {
            t.entities.media.forEach(media => {
                index_map[media.indices[0]] = [media.indices[1], text => ``];
            });
        }
    };

    let display_start = t.display_text_range !== undefined ? t.display_text_range[0] : 0;
    let display_end   = t.display_text_range !== undefined ? t.display_text_range[1] : full_text_array.length;
    for (let [current_pos, _] of full_text_array.entries()) {
        if (current_pos < display_start) { // do not render first part of message
            last_pos = current_pos + 1; // to start copy from next symbol
            continue;
        }
        if (current_pos == display_end ||                // reached the end of visible part
            current_pos == full_text_array.length - 1) { // reached the end of tweet itself
                if (display_end == full_text_array.length) current_pos++; // dirty hack to include last element of slice
                result += escapeHTML(full_text_array.slice(last_pos, current_pos).join(''));
                break;
        }
        if (current_pos > display_end) {
            break; // do not render last part of message
        }

        if (current_pos in index_map) {
            let [end, func] = index_map[current_pos];
            
            if (current_pos > last_pos) {
                result += escapeHTML(full_text_array.slice(last_pos, current_pos).join('')); // store chunk of untouched text
            }
            result += func(full_text_array.slice(current_pos, end).join('')); // run replacer func on corresponding range
            last_pos = end;
        }
    }
    return result
}
function arrayInsert(arr, index, value) {
    return [...arr.slice(0, index), value, ...arr.slice(index)];
}
function generatePoll(tweet, tweetElement, user) {
    let pollElement = tweetElement.getElementsByClassName('tweet-card')[0];
    pollElement.innerHTML = '';
    let poll = tweet.card.binding_values;
    let choices = Object.keys(poll).filter(key => key.endsWith('label')).map((key, i) => ({
        label: poll[key].string_value,
        count: poll[key.replace('label', 'count')] ? +poll[key.replace('label', 'count')].string_value : 0,
        id: parseInt(key.replace(/[^0-9]/g, ''))
    }));
    choices.sort((a, b) => a.id - b.id);
    let voteCount = choices.reduce((acc, cur) => acc + cur.count, 0);
    if(poll.selected_choice || user.id_str === tweet.user.id_str || (poll.counts_are_final && poll.counts_are_final.boolean_value)) {
        for(let i in choices) {
            let choice = choices[i];
            if(user.id_str !== tweet.user.id_str && poll.selected_choice && choice.id === +poll.selected_choice.string_value) {
                choice.selected = true;
            }
            choice.percentage = Math.round(choice.count / voteCount * 100) || 0;
            let choiceElement = document.createElement('div');
            choiceElement.classList.add('choice');
            choiceElement.innerHTML = html`
                <div class="choice-bg" style="width:${choice.percentage}%" data-percentage="${choice.percentage}"></div>
                <div class="choice-label">
                    <span>${escapeHTML(choice.label)}</span>
                    ${choice.selected ? `<span class="choice-selected"></span>` : ''}
                </div>
                ${isFinite(choice.percentage) ? `<div class="choice-count">${choice.count} (${choice.percentage}%)</div>` : '<div class="choice-count">0</div>'}
            `;
            pollElement.append(choiceElement);
        }
    } else {
        for(let i in choices) {
            let choice = choices[i];
            let choiceElement = document.createElement('div');
            choiceElement.classList.add('choice', 'choice-unselected');
            choiceElement.classList.add('tweet-button');
            choiceElement.innerHTML = html`
                <div class="choice-bg" style="width:100%"></div>
                <div class="choice-label">${escapeHTML(choice.label)}</div>
            `;
            choiceElement.addEventListener('click', async () => {
                let newCard = await API.tweet.vote(poll.api.string_value, tweet.id_str, tweet.card.url, tweet.card.name, choice.id);
                tweet.card = newCard.card;
                generateCard(tweet, tweetElement, user);
            });
            pollElement.append(choiceElement);
        }
    }
    if(tweet.card.url.startsWith('card://')) {
        let footer = document.createElement('span');
        footer.classList.add('poll-footer');
        let endsAtMessage = `Ends at: ${new Date(poll.end_datetime_utc.string_value).toLocaleString()}`;
        footer.innerHTML = html`${voteCount} ${voteCount === 1 ? 'vote' : 'votes'}${(!poll.counts_are_final || !poll.counts_are_final.boolean_value) && poll.end_datetime_utc ? ` ・ ${endsAtMessage}` : ''}`;
        pollElement.append(footer);
    }
}
function generateCard(tweet, tweetElement, user) {
    if(!tweet.card) return;
    if(tweet.card.name === 'promo_image_convo' || tweet.card.name === 'promo_video_convo') {
        let vals = tweet.card.binding_values;
        let a = document.createElement('a');
        a.title = vals.thank_you_text.string_value;
        if(tweet.card.name === 'promo_image_convo') {
            a.href = vals.thank_you_url ? vals.thank_you_url.string_value : "#";
            a.target = '_blank';
            let img = document.createElement('img');
            let imgValue = vals.promo_image;
            if(!imgValue) {
                imgValue = vals.cover_promo_image_original;
            }
            if(!imgValue) {
                imgValue = vals.cover_promo_image_large;
            }
            if(!imgValue) {
                return;
            }
            img.src = imgValue.image_value.url;
            let [w, h] = sizeFunctions[1](imgValue.image_value.width, imgValue.image_value.height);
            img.width = w;
            img.height = h;
            img.className = 'tweet-media-element';
            a.append(img);
        } else {
            let overlay = document.createElement('div');
            overlay.innerHTML = html`
                <svg viewBox="0 0 24 24" class="tweet-media-video-overlay-play">
                    <g>
                        <path class="svg-play-path" d="M8 5v14l11-7z"></path>
                        <path d="M0 0h24v24H0z" fill="none"></path>
                    </g>
                </svg>
            `;
            overlay.className = 'tweet-media-video-overlay';
            overlay.addEventListener('click', async e => {
                e.preventDefault();
                e.stopImmediatePropagation();
                try {
                    let res = await GM_fetch(vid.currentSrc); // weird problem with vids breaking cuz twitter sometimes doesnt send content-length
                    if(!res.headers.get('content-length')) await sleep(1000);
                } catch(e) {
                    console.error(e);
                }
                vid.play();
                vid.controls = true;
                vid.classList.remove('tweet-media-element-censor');
                overlay.style.display = 'none';
            });
            let vid = document.createElement('video');
            let [w, h] = sizeFunctions[1](vals.player_image_original.image_value.width, vals.player_image_original.image_value.height);
            vid.width = w;
            vid.height = h;
            vid.preload = 'none';
            vid.poster = vals.player_image_large.image_value.url;
            vid.className = 'tweet-media-element';
            vid.addEventListener('click', async e => {
                e.preventDefault();
                e.stopImmediatePropagation();
            });
            GM_fetch(vals.player_stream_url.string_value).then(res => res.text()).then(blob => {
                let xml = new DOMParser().parseFromString(blob, 'text/xml');
                let MediaFile = xml.getElementsByTagName('MediaFile')[0];
                vid.src = MediaFile.textContent.trim();
            });
            let tweetMedia = document.createElement('div');
            tweetMedia.className = 'tweet-media';
            tweetMedia.style.right = 'unset';
            tweetMedia.append(overlay, vid);
            a.append(tweetMedia);
        }
        let ctas = [];
        if(vals.cta_one) {
            ctas.push([vals.cta_one, vals.cta_one_tweet]);
        }
        if(vals.cta_two) {
            ctas.push([vals.cta_two, vals.cta_two_tweet]);
        }
        if(vals.cta_three) {
            ctas.push([vals.cta_three, vals.cta_three_tweet]);
        }
        if(vals.cta_four) {
            ctas.push([vals.cta_four, vals.cta_four_tweet]);
        }
    } else if(tweet.card.name === "player") {
        let iframe = document.createElement('iframe');
        iframe.src = tweet.card.binding_values.player_url.string_value.replace("youtube.com", "youtube-nocookie.com").replace("autoplay=true", "autoplay=false").replace("autoplay=1", "autoplay=0");
        iframe.classList.add('tweet-player');
        let [w, h] = sizeFunctions[1](+tweet.card.binding_values.player_width.string_value, +tweet.card.binding_values.player_height.string_value);
        iframe.width = w;
        iframe.height = h;
        iframe.loading = 'lazy';
        iframe.allowFullscreen = true;
        tweetElement.getElementsByClassName('tweet-card')[0].innerHTML = '';
        tweetElement.getElementsByClassName('tweet-card')[0].append(iframe);
    } else if(tweet.card.name === "unified_card") {
        let uc = JSON.parse(tweet.card.binding_values.unified_card.string_value);
        for(let cn of uc.components) {
            let co = uc.component_objects[cn];
            if(co.type === "media") {
                let media = uc.media_entities[co.data.id];

                if(media.type === "photo") {
                    let img = document.createElement('img');
                    img.className = 'tweet-media-element';
                    let [w, h] = sizeFunctions[1](media.original_info.width, media.original_info.height);
                    img.width = w;
                    img.height = h;
                    img.loading = 'lazy';
                    img.src = media.media_url_https;
                    img.addEventListener('click', () => {
                        new Viewer(img, {
                            transition: false,
                            zoomRatio: 0.3
                        });
                    });
                    tweetElement.getElementsByClassName('tweet-card')[0].append(img, document.createElement('br'));
                } else if(media.type === "animated_gif" || media.type === "video") {
                    let video = document.createElement('video');
                    video.className = 'tweet-media-element tweet-media-element-one';
                    let [w, h] = sizeFunctions[1](media.original_info.width, media.original_info.height);
                    video.width = w;
                    video.height = h;
                    video.crossOrigin = 'anonymous';
                    video.loading = 'lazy';
                    video.controls = true;
                    if(!media.video_info) {
                        console.log(`bug found in ${tweet.id_str}, please report this message to https://github.com/dimdenGD/OldTwitter/issues`, tweet);
                        continue;
                    };
                    let variants = media.video_info.variants.sort((a, b) => {
                        if(!b.bitrate) return -1;
                        return b.bitrate-a.bitrate;
                    });
                    for(let v in variants) {
                        let source = document.createElement('source');
                        source.src = variants[v].url;
                        source.type = variants[v].content_type;
                        video.append(source);
                    }
                    tweetElement.getElementsByClassName('tweet-card')[0].append(video, document.createElement('br'));
                }
            } else if(co.type === "app_store_details") {
                let app = uc.app_store_data[uc.destination_objects[co.data.destination].data.app_id][0];
                let appElement = document.createElement('div');
                appElement.classList.add('tweet-app-info');
                appElement.innerHTML = html`
                    <h3>${escapeHTML(app.title.content)}</h3>
                    <span>${escapeHTML(app.category.content)}</span>
                    <br>
                `;
                tweetElement.getElementsByClassName('tweet-card')[0].append(appElement);
            } else if(co.type === "button_group") {
                let buttonGroup = document.createElement('div');
                buttonGroup.classList.add('tweet-button-group');
                for(let b of co.data.buttons) {
                    let app = uc.app_store_data[uc.destination_objects[b.destination].data.app_id][0];
                    let button = document.createElement('a');
                    button.href = `http://play.google.com/store/apps/details?id=${app.id}`;
                    button.target = '_blank';
                    button.className = `nice-button tweet-app-button tweet-app-button-${b.style}`
                    button.innerText = b.action[0].toUpperCase() + b.action.slice(1);
                    buttonGroup.append(button);
                }
                tweetElement.getElementsByClassName('tweet-card')[0].append(buttonGroup);
            }
        }
    } else if(tweet.card.name === "summary" || tweet.card.name === "summary_large_image") {
        let vals = tweet.card.binding_values;
        let a = document.createElement('a');
        let url = vals.card_url.string_value;
        if(tweet.entities && tweet.entities.urls) {
            let urlEntity = tweet.entities.urls.find(u => u.url === url);
            if(urlEntity) {
                url = urlEntity.expanded_url;
            }
        }
        a.target = '_blank';
        a.href = url;
        a.className = 'tweet-card-link yeah-box';
        a.innerHTML = html`
            ${vals.thumbnail_image ? `<img src="${vals.thumbnail_image.image_value.url}" class="tweet-card-link-thumbnail">` : ''}
            <div class="tweet-card-link-text">
                ${vals.vanity_url ? `<span class="tweet-card-link-vanity">${escapeHTML(vals.vanity_url.string_value)}</span><br>` : ''}
                ${vals.title ? `<h3 class="tweet-card-link-title">${escapeHTML(vals.title.string_value)}</h3>` : ''}
                ${vals.description ? `<span class="tweet-card-link-description">${escapeHTML(vals.description.string_value)}</span>` : ''}
            </div>
        `;
        tweetElement.getElementsByClassName('tweet-card')[0].append(a);
    } else if(tweet.card.url.startsWith('card://')) {
        generatePoll(tweet, tweetElement, user);
    }
}
function createEmojiPicker(container, input, style = {}) {
    let picker = new EmojiPicker();
    for(let i in style) {
        picker.style[i] = style[i];
    }
    picker.className = isDarkModeEnabled ? 'dark' : 'light';
    picker.addEventListener('emoji-click', e => {
        let pos = input.selectionStart;
        let text = input.value;
        input.value = text.slice(0, pos) + e.detail.unicode + text.slice(pos);
        input.selectionStart = pos + e.detail.unicode.length;
    });
    container.append(picker);

    let observer;

    setTimeout(() => {
        function oc (e) {
            if (picker.contains(e.target)) return;
            if(observer) {
                observer.disconnect();
            }
            picker.remove();
            document.removeEventListener('click', oc);
            picker.database.close();
        }
        document.addEventListener('click', oc);
        picker.shadowRoot.querySelector("input.search").focus();
    }, 100);

    return picker;
}
function isEmojiOnly(str) {
    const stringToTest = str.replace(/ /g,'');
    const emojiRegex = /^(?:(?:\p{RI}\p{RI}|\p{Emoji}(?:\p{Emoji_Modifier}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?(?:\u{200D}\p{Emoji}(?:\p{Emoji_Modifier}|\u{FE0F}\u{20E3}?|[\u{E0020}-\u{E007E}]+\u{E007F})?)*)|[\u{1f900}-\u{1f9ff}\u{2600}-\u{26ff}\u{2700}-\u{27bf}])+$/u;
    return emojiRegex.test(stringToTest) && Number.isNaN(Number(stringToTest));
}

function renderMedia(t) {
    let _html = '';
    if(!t.extended_entities || !t.extended_entities.media) return '';

    let cws = [];

    for(let i = 0; i < t.extended_entities.media.length; i++) {
        let m = t.extended_entities.media[i];
        let toCensor = t.possibly_sensitive;
        if(m.type === 'photo') {
            let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height);
            _html += html`
            <img 
                ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text.replaceAll('"', "'"))}" title="${escapeHTML(m.ext_alt_text.replaceAll('"', "'"))}"` : ''}
                crossorigin="anonymous"
                width="${w}"
                height="${h}"
                loading="lazy"
                src="${m.media_url_https + (false && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' ? '?name=small' : '')}"
                class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
            >`;
        } else if(m.type === 'animated_gif') {
            let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height);
            let rid = m.id_str + m.media_key;
            _html += html`
                <video
                    ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
                    crossorigin="anonymous"
                    width="${w}"
                    height="${h}"
                    loop
                    disableRemotePlayback
                    autoplay
                    muted
                    class="tweet-media-element tweet-media-gif ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
                >
                    ${m.video_info.variants.map(v => `<source src="${v.url}" type="${v.content_type}">`).join('\n')}
                    Unsupported video
                </video>
            `;
        } else if(m.type === 'video') {
            if(m.mediaStats && m.mediaStats.viewCount) {
                m.ext = {
                    mediaStats: { r: { ok: { viewCount: m.mediaStats.viewCount } } }
                }
            }
            let [w, h] = sizeFunctions[t.extended_entities.media.length](m.original_info.width, m.original_info.height);
            _html += html`
                <video
                    ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''}
                    crossorigin="anonymous"
                    width="${w}"
                    height="${h}"
                    preload="none"
                    disableRemotePlayback
                    ${t.extended_entities.media.length > 1 ? 'controls' : ''}
                    poster="${m.media_url_https}"
                    class="tweet-media-element ${mediaClasses[t.extended_entities.media.length]} ${toCensor ? 'tweet-media-element-censor' : ''}"
                >
                    ${m.video_info.variants.map(v => `<source src="${v.url}" type="${v.content_type}">`).join('\n')}
                    Unsupported video
                </video>
            `;
        }
        if(i === 1 && t.extended_entities.media.length > 3) {
            _html += '<br>';
        }
    }

    if(cws.length > 0) {
        cws = [...new Set(cws)];
        cws = "Content warnings: " + cws.join(', ');
        _html += html`<br><div class="tweet-media-cws">${cws}</div>`;
    }
    return _html;
}


function openInNewTab(href) {
    Object.assign(document.createElement('a'), {
        target: '_blank',
        rel: 'noopener noreferrer',
        href: href,
    }).click();
}

function sleep(ms) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

async function appendTweet(t, timelineContainer, options = {}, user) {
    if(typeof t !== 'object') {
        console.error('Tweet is undefined', t, timelineContainer, options);
        return;
    }
    if(typeof t.user !== 'object') {
        console.error('Tweet user is undefined', t, timelineContainer, options);
        return;
    }
    try {
        if(typeof seenReplies !== 'undefined' && !options.ignoreSeen) {
            if(seenReplies.includes(t.id_str)) return;
            seenReplies.push(t.id_str);
        }
        if(typeof seenThreads !== 'undefined' && !options.ignoreSeen) {
            if(seenThreads.includes(t.id_str)) return;
        }
        // verification
        if(t.user.ext_verified_type) {
            t.user.verified_type = t.user.ext_verified_type;
            t.user.verified = true;
        }
        if(t.user.ext && t.user.ext.isBlueVerified && t.user.ext.isBlueVerified.r && t.user.ext.isBlueVerified.r.ok) {
            t.user.verified_type = "Blue";
            t.user.verified = true;
        }
        if(t.user && t.user.ext && t.user.ext.verifiedType && t.user.ext.verifiedType.r && t.user.ext.verifiedType.r.ok) {
            t.user.verified_type = t.user.ext.verifiedType.r.ok;
            t.user.verified = true;
        }
        if(t.quoted_status && t.quoted_status.user.verified_type === "Blue") {
            delete t.quoted_status.user.verified_type;
            t.quoted_status.user.verified = false;
        }

        const tweet = document.createElement('div');
        tweet.tweet = t;
        t.element = tweet;
        t.options = options;

        if(!options.mainTweet && typeof mainTweetLikers !== 'undefined' && !location.pathname.includes("retweets/with_comments") && !document.querySelector('.modal')) {
            tweet.addEventListener('click', async e => {
                if (!e.target.closest(".tweet-button") && !e.target.closest(".tweet-body-text-span") && !e.target.closest(".tweet-edit-section") && !e.target.closest(".dropdown-menu") && !e.target.closest(".tweet-media-element") && !e.target.closest("a") && !e.target.closest("button")) {
                    document.getElementById('loading-box').hidden = false;
                    savePageData();
                    history.pushState({}, null, `/${t.user.screen_name}/status/${t.id_str}`);
                    updateSubpage();
                    mediaToUpload = [];
                    linkColors = {};
                    cursor = undefined;
                    seenReplies = [];
                    mainTweetLikers = [];
                    let restored = await restorePageData();
                    let id = location.pathname.match(/status\/(\d{1,32})/)[1];
                    if(subpage === 'tweet' && !restored) {
                        updateReplies(id);
                    } else if(subpage === 'likes') {
                        updateLikes(id);
                    } else if(subpage === 'retweets') {
                        updateRetweets(id);
                    } else if(subpage === 'retweets_with_comments') {
                        updateRetweetsWithComments(id);
                    }
                    renderDiscovery();
                    renderTrends();
                    currentLocation = location.pathname;
                }
            });
        } else {
            if(!options.mainTweet) {
                tweet.addEventListener('click', e => {
                    if(!e.target.closest(".tweet-button") && !e.target.closest(".tweet-body-text-span") && !e.target.closest(".tweet-edit-section") && !e.target.closest(".dropdown-menu") && !e.target.closest(".tweet-media-element") && !e.target.closest("a") && !e.target.closest("button")) {
                        let tweetData = t;
                        if(tweetData.retweeted_status) tweetData = tweetData.retweeted_status;
                        tweet.classList.add('tweet-preload');
                        let selection = window.getSelection();
                        if(selection.toString().length > 0 && selection.focusNode && selection.focusNode.closest(`div.tweet[data-tweet-id="${tweetData.id_str}"]`)) {
                            return;
                        }
                        let a = document.createElement('a');
                        a.href = `/${tweetData.user.screen_name}/status/${tweetData.id_str}`;
                        a.target = '_blank';
                        a.click();
                    }
                });
            }
        }
        tweet.addEventListener('mousedown', e => {
            if(e.button === 1) {
                // tweet-media-element is clickable, since it should open the tweet in a new tab.
                if(!e.target.closest(".tweet-button") && !e.target.closest(".tweet-edit-section") && !e.target.closest(".dropdown-menu") && !e.target.closest("a") && !e.target.closest("button")) {
                    e.preventDefault();
                    openInNewTab(`/${t.user.screen_name}/status/${t.id_str}`);
                }
            }
        });
        tweet.tabIndex = -1;
        tweet.className = `yeah-tweet ${options.mainTweet ? 'tweet-main' : location.pathname.includes('/status/') ? 'tweet-replying' : ''}`.trim();
        tweet.dataset.tweetId = t.id_str;
        tweet.dataset.userId = t.user.id_str;
        try {
            if(!activeTweet) {
                tweet.classList.add('tweet-active');
                activeTweet = tweet;
            }
        } catch(e) {};

        if(t.nonReply) {
            tweet.classList.add('tweet-non-reply');
        }

        if(t.threadContinuation) {
            options.threadContinuation = true;
        }
        if(t.noTop) {
            options.noTop = true;
        }
        if (options.threadContinuation) tweet.classList.add('tweet-self-thread-continuation');
        if (options.selfThreadContinuation) tweet.classList.add('tweet-self-thread-continuation');

        if (options.noTop) tweet.classList.add('tweet-no-top');
        let full_text = t.full_text ? t.full_text : '';
        let tweetLanguage = t.lang; // originally i used i18n api to detect languages simply because i didn't know of t.lang existence
        if(!tweetLanguage) {
            tweetLanguage = 'und';
        }
        if(tweetLanguage.includes('-')) {
            let [lang, country] = tweetLanguage.split('-');
            tweetLanguage = `${lang}_${country.toUpperCase()}`;
        }
        let videos = t.extended_entities && t.extended_entities.media && t.extended_entities.media.filter(m => m.type === 'video');
        if(!videos || videos.length === 0) {
            videos = undefined;
        }
        if(videos) {
            for(let v of videos) {
                if(!v.video_info) continue;
                v.video_info.variants = v.video_info.variants.sort((a, b) => {
                    if(!b.bitrate) return -1;
                    return b.bitrate-a.bitrate;
                });
            }
        }
        if(full_text.includes("Learn more")) {
            console.log(t);
        }
        if(t.withheld_in_countries && (t.withheld_in_countries.includes("XX") || t.withheld_in_countries.includes("XY"))) {
            full_text = "";
        }
        if(!t.quoted_status) { //t.quoted_status is undefined if the user blocked the quoter (this also applies to deleted/private tweets too, but it just results in original behavior then)
            try {
                if(t.quoted_status_result && t.quoted_status_result.result.tweet) {
                    t.quoted_status = t.quoted_status_result.result.tweet.legacy;
                    t.quoted_status.user = t.quoted_status_result.result.tweet.core.user_results.result.legacy;
                }/* else if(t.quoted_status_id_str) {
                    t.quoted_status = await API.tweet.getV2(t.quoted_status_id_str);
                    console.log(t.quoted_status);
                }*/
            } catch {
                t.quoted_status = undefined;
            }
        }
        let mentionedUserText = ``;
        let quoteMentionedUserText = ``;
        if(t.in_reply_to_screen_name && t.display_text_range) {
            t.entities.user_mentions.forEach(user_mention => {
                if(user_mention.indices[0] < t.display_text_range[0]){
                    mentionedUserText += `<a href="/${user_mention.screen_name}">@${user_mention.screen_name}</a> `
                }
                //else this is not reply but mention
            });
        }
        if(t.quoted_status && t.quoted_status.in_reply_to_screen_name && t.display_text_range) {
            t.quoted_status.entities.user_mentions.forEach(user_mention => {
                if(user_mention.indices[0] < t.display_text_range[0]){
                    quoteMentionedUserText += `@${user_mention.screen_name} `
                }
                //else this is not reply but mention
            });
        }
        // i fucking hate this thing
        tweet.innerHTML = html`
            <div class="tweet-top" hidden></div>
            <a class="tweet-avatar-link" href="/${t.user.screen_name}">
                <img
                    src="${`${t.user.profile_image_url_https}`.replace("_normal.", "_bigger.")}"
                    alt="${t.user.name}"
                    class="tweet-avatar"
                    width="48"
                    height="48"
                >
            </a>
            <div class="tweet-header ${options.mainTweet ? 'tweet-header-main' : ''}">
                <a class="tweet-header-info ${options.mainTweet ? 'tweet-header-info-main' : ''}" href="/${t.user.screen_name}">
                    <b
                        ${t.user.id_str === '1708130407663759360' ? 'title="Old Twitter Layout extension developer" ' : ''}
                        class="tweet-header-name ${options.mainTweet ? 'tweet-header-name-main' : ''} ${t.user.verified || t.user.verified_type ? 'user-verified' : t.user.id_str === '1708130407663759360' ? 'user-verified user-verified-dimden' : ''} ${t.user.protected ? 'user-protected' : ''} ${t.user.verified_type === 'Government' ? 'user-verified-gray' : t.user.verified_type === 'Business' ? 'user-verified-yellow' : t.user.verified_type === 'Blue' ? 'user-verified-blue' : ''}"
                    >${escapeHTML(t.user.name)}</b>
                    <span class="tweet-header-handle">@${t.user.screen_name}</span>
                </a>
                <a class="tweet-time" data-timestamp="${new Date(t.created_at).getTime()}" title="${new Date(t.created_at).toLocaleString()}" href="/${t.user.screen_name}/status/${t.id_str}">${timeElapsed(new Date(t.created_at).getTime())}</a>
            </div>
            <article class="tweet-body ${options.mainTweet ? 'tweet-body-main' : ''}">
                ${mentionedUserText !== `` &&
                    !options.threadContinuation &&
                    !options.noTop &&
                    !location.pathname.includes('/status/') ? html`
                <div class="tweet-reply-to"><span>${"Replying to $SCREEN_NAME$".replace('$SCREEN_NAME$', mentionedUserText.trim().replaceAll(`> <`, `>${", "}<`).replace(`>${", "}<`, `>${" and "}<`))}</span></div>
                `: ''}
                <div lang="${t.lang}" class="tweet-body-text tweet-body-text-long">
                    <span class="tweet-body-text-span">${full_text ? await renderTweetBodyHTML(t) : ''}</span>
                </div>
                ${t.extended_entities && t.extended_entities.media ? html`
                    <div class="tweet-media">
                        ${t.extended_entities.media.length === 1 && t.extended_entities.media[0].type === 'video' ? html`
                            <div class="tweet-media-video-overlay tweet-button">
                                <svg viewBox="0 0 24 24" class="tweet-media-video-overlay-play">
                                    <g>
                                        <path class="svg-play-path" d="M8 5v14l11-7z"></path>
                                        <path d="M0 0h24v24H0z" fill="none"></path>
                                    </g>
                                </svg>
                            </div>
                        ` : ''}
                        ${renderMedia(t)}
                    </div>
                    ${t.extended_entities && t.extended_entities.media && t.extended_entities.media.some(m => m.type === 'animated_gif') ? html`<div class="tweet-media-controls">GIF</div>` : ''}
                    <span class="tweet-media-data"></span>
                ` : ``}
                ${t.card ? `<div class="tweet-card"></div>` : ''}
                ${t.quoted_status ? html`
                <a class="tweet-body-quote" target="_blank" href="/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}">
                    <img src="${t.quoted_status.user.profile_image_url_https}" alt="${escapeHTML(t.quoted_status.user.name)}" class="tweet-avatar-quote" width="24" height="24">
                    <div class="tweet-header-quote">
                        <span class="tweet-header-info-quote">
                        <b class="tweet-header-name-quote ${t.quoted_status.user.verified ? 'user-verified' : t.quoted_status.user.id_str === '1708130407663759360' ? 'user-verified user-verified-dimden' : ''} ${t.quoted_status.user.protected ? 'user-protected' : ''} ${t.quoted_status.user.verified_type === 'Government' ? 'user-verified-gray' : t.quoted_status.user.verified_type === 'Business' ? 'user-verified-yellow' : t.quoted_status.user.verified_type === 'Blue' ? 'user-verified-blue' : ''}">${escapeHTML(t.quoted_status.user.name)}</b>
                        <span class="tweet-header-handle-quote">@${t.quoted_status.user.screen_name}</span>
                        </span>
                    </div>
                    <span class="tweet-time-quote" data-timestamp="${new Date(t.quoted_status.created_at).getTime()}" title="${new Date(t.quoted_status.created_at).toLocaleString()}">${timeElapsed(new Date(t.quoted_status.created_at).getTime())}</span>
                    ${quoteMentionedUserText !== `` ? html`
                    <span class="tweet-reply-to tweet-quote-reply-to">${"Replying to $SCREEN_NAME$".replace('$SCREEN_NAME$', quoteMentionedUserText.trim().replaceAll(` `,", ").replace(", "," and "))}</span>
                    ` : ''}
                    <span class="tweet-body-text tweet-body-text-quote tweet-body-text-long" style="color:var(--yeah-default-text-color)!important">${t.quoted_status.full_text ? await renderTweetBodyHTML(t, true) : ''}</span>
                    ${t.quoted_status.extended_entities && t.quoted_status.extended_entities.media ? html`
                    <div class="tweet-media-quote">
                        ${t.quoted_status.extended_entities.media.map(m => `<${m.type === 'photo' ? 'img' : 'video'} ${m.ext_alt_text ? `alt="${escapeHTML(m.ext_alt_text)}" title="${escapeHTML(m.ext_alt_text)}"` : ''} crossorigin="anonymous" width="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[0]}" height="${quoteSizeFunctions[t.quoted_status.extended_entities.media.length](m.original_info.width, m.original_info.height)[1]}" loading="lazy" ${m.type === 'video' ? 'disableRemotePlayback controls' : ''} ${m.type === 'animated_gif' ? 'disableRemotePlayback loop muted onclick="if(this.paused) this.play(); else this.pause()"' : ''}${m.type === 'animated_gif' ? ' autoplay' : ''} src="${m.type === 'photo' ? m.media_url_https + (false && (m.media_url_https.endsWith('.jpg') || m.media_url_https.endsWith('.png')) ? '?name=orig' : window.navigator && navigator.connection && navigator.connection.type === 'cellular' ? '?name=small' : '') : m.video_info.variants.find(v => v.content_type === 'video/mp4').url}" class="tweet-media-element tweet-media-element-quote ${m.type === 'animated_gif' ? 'tweet-media-element-quote-gif' : ''} ${mediaClasses[t.quoted_status.extended_entities.media.length]}">${m.type === 'photo' ? '' : '</video>'}`).join('\n')}
                    </div>
                    ` : ''}
                </a>
                ` : ``}
                ${t.limited_actions === 'limit_trusted_friends_tweet' && (options.mainTweet || !location.pathname.includes('/status/')) ? html`
                <div class="tweet-limited">
                    ${"This tweet is visible only to people who are in @$SCREEN_NAME$'s trusted friends circle."}
                    <a href="https://help.twitter.com/en/using-twitter/twitter-circle" target="_blank">${"Learn more."}</a>
                </div>
                `.replace('$SCREEN_NAME$', tweet.trusted_circle_owner ? tweet.trusted_circle_owner : tweetStorage[t.conversation_id_str] ? tweetStorage[t.conversation_id_str].user.screen_name : t.in_reply_to_screen_name ? t.in_reply_to_screen_name : t.user.screen_name) : ''}
                ${t.tombstone ? `<div class="tweet-warning">${t.tombstone}</div>` : ''}
                <a ${!options.mainTweet ? 'hidden' : ''} class="tweet-date" title="${new Date(t.created_at).toLocaleString()}" href="/${t.user.screen_name}/status/${t.id_str}"><br>${new Date(t.created_at).toLocaleTimeString(undefined, { hour: 'numeric', minute: 'numeric' }).toLowerCase()} - ${new Date(t.created_at).toLocaleDateString(undefined, { day: 'numeric', month: 'short', year: 'numeric' })}  ・ ${t.source ? t.source.split('>')[1].split('<')[0] : 'Unknown'}</a>
                <div class="tweet-interact">
                    <span class="tweet-button tweet-interact-reply" title="Replies" data-val="${t.reply_count}">${options.mainTweet ? '' : formatLargeNumber(t.reply_count).replace(/\s/g, ',')}</span>
                    <span title="Retweets" class="tweet-button tweet-interact-retweet${t.retweeted ? ' tweet-interact-retweeted' : ''}${(t.user.protected || t.limited_actions === 'limit_trusted_friends_tweet') && t.user.id_str !== user.id_str ? ' tweet-interact-retweet-disabled' : ''}" data-val="${t.retweet_count}">${options.mainTweet ? '' : formatLargeNumber(t.retweet_count).replace(/\s/g, ',')}</span>
                    <span title="Likes" class="tweet-button tweet-yeah-interact-favorite ${t.favorited ? 'tweet-yeah-interact-favorited' : ''}" data-val="${t.favorite_count}">${options.mainTweet ? '' : formatLargeNumber(t.favorite_count).replace(/\s/g, ',')}</span>
                    ${t.ext && t.ext.views && t.ext.views.r && t.ext.views.r.ok && t.ext.views.r.ok.count ? html`<span title="${"Views"}" class="tweet-interact-views tweet-button" data-val="${t.ext.views.r.ok.count}">${formatLargeNumber(t.ext.views.r.ok.count).replace(/\s/g, ',')}</span>` : ''}
                </div>
            </article>
        `;
        // gifs
        let gifs = Array.from(tweet.querySelectorAll('.tweet-media-gif, .tweet-media-element-quote-gif'));
        if(gifs.length) {
            gifs.forEach(gif => {
                gif.addEventListener('click', () => {
                    if(gif.paused) gif.play();
                    else gif.pause();
                });
            });
        }
        // video
        let vidOverlay = tweet.getElementsByClassName('tweet-media-video-overlay')[0];
        if(vidOverlay) {
            vidOverlay.addEventListener('click', async () => {
                let vid = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO')[0];
                try {
                    let res = await GM_fetch(vid.currentSrc); // weird problem with vids breaking cuz twitter sometimes doesnt send content-length
                    if(!res.headers.get('content-length')) await sleep(1000);
                } catch(e) {
                    console.error(e);
                }
                vid.play();
                vid.controls = true;
                vid.classList.remove('tweet-media-element-censor');
                vidOverlay.style.display = 'none';
            });
        }
        if(videos) {
            let videoErrors = 0;
            let vids = Array.from(tweet.getElementsByClassName('tweet-media')[0].children).filter(e => e.tagName === 'VIDEO');
            vids[0].addEventListener('error', () => {
                if(videoErrors >= 3) return;
                videoErrors++;
                setTimeout(() => {
                    vids[0].load();
                }, 25);
            })
            for(let vid of vids) {
                vid.addEventListener('mousedown', e => {
                    if(e.button === 1) {
                        e.preventDefault();
                        window.open(vid.currentSrc, '_blank');
                    }
                });
            }
        }

        if(t.card) {
            generateCard(t, tweet, user);
        }
        if (options.top) {
            tweet.querySelector('.tweet-top').hidden = false;
            const icon = document.createElement('span');
            icon.innerText = options.top.icon;
            icon.classList.add('tweet-top-icon');
            icon.style.color = options.top.color;

            const span = document.createElement("span");
            span.classList.add("tweet-top-text");
            span.innerHTML = options.top.text;
            if(options.top.class) {
                span.classList.add(options.top.class);
                tweet.classList.add(`tweet-top-${options.top.class}`);
            }
            tweet.querySelector('.tweet-top').append(icon, span);
        }

        const tweetBodyQuote = tweet.getElementsByClassName('tweet-body-quote')[0];
        const tweetMediaQuote = tweet.getElementsByClassName('tweet-media-quote')[0];
        const tweetInteract = tweet.getElementsByClassName('tweet-interact')[0];
        const tweetFooter = tweet.getElementsByClassName('tweet-footer')[0];

        // community notes
        if(t.birdwatch) {
            if(t.birdwatch.subtitle) {
                let div = document.createElement('div');
                div.classList.add('tweet-birdwatch', 'box');
                let text = Array.from(escapeHTML(t.birdwatch.subtitle.text));
                for(let e = t.birdwatch.subtitle.entities.length - 1; e >= 0; e--) {
                    let entity = t.birdwatch.subtitle.entities[e];
                    if(!entity.ref) continue;
                    text = arrayInsert(text, entity.toIndex, '</a>');
                    text = arrayInsert(text, entity.fromIndex, `<a href="${entity.ref.url}" target="_blank">`);
                }
                text = text.join('');
                
                div.innerHTML = html`
                    <div class="tweet-birdwatch-header">
                        <span class="tweet-birdwatch-title">${escapeHTML(t.birdwatch.title)}</span>
                    </div>
                    <div class="tweet-birdwatch-body">
                        <span class="tweet-birdwatch-subtitle">${text}</span>
                    </div>
                `;
    
                if(tweetFooter) tweetFooter.before(div);
                else tweetInteract.before(div);
            }
        }

        // Quote body
        if(tweetMediaQuote) tweetMediaQuote.addEventListener('click', e => {
            if(e && e.target && e.target.tagName === "VIDEO") {
                e.preventDefault();
                e.stopPropagation();
                e.stopImmediatePropagation();
                if(e.target.paused) {
                    e.target.play();
                } else {
                    e.target.pause();
                }
            }
        });
        if(tweetBodyQuote) {
            tweetBodyQuote.addEventListener('click', e => {
                e.preventDefault();
                let a = document.createElement('a');
                a.href = `/${t.quoted_status.user.screen_name}/status/${t.quoted_status.id_str}`;
                a.target = '_blank';
                a.click();
            });
        }

        // Media
        if (t.extended_entities && t.extended_entities.media) {
            const tweetMedia = tweet.getElementsByClassName('tweet-media')[0];
            tweetMedia.addEventListener('click', e => {
                if (e.target.className && e.target.className.includes('tweet-media-element-censor')) {
                    return e.target.classList.remove('tweet-media-element-censor');
                }
                if (e.target.tagName === 'IMG') {
                    if(!e.target.src.includes('?name=') && !e.target.src.endsWith(':orig') && !e.target.src.startsWith('data:')) {
                        e.target.src += '?name=orig';
                    } else if(e.target.src.includes('?name=small')) {
                        e.target.src = e.target.src.replace('?name=small', '?name=large');
                    }
                    new Viewer(tweetMedia, {
                        transition: false,
                        zoomRatio: 0.3
                    });
                    e.target.click();
                }
            });
        }

        if(options.noInsert) {
            return tweet;
        }

        if(options.after) {
            options.after.after(tweet);
        } else if (options.before) {
            options.before.before(tweet);
        } else if (options.prepend) {
            timelineContainer.prepend(tweet);
        } else {
            timelineContainer.append(tweet);
        }
        return tweet;
    } catch(e) {
        console.error(e);
        if(Date.now() - lastTweetErrorDate > 1000) {
            lastTweetErrorDate = Date.now();
            createModal(/*html*/`
                <div style="max-width:700px">
                    <span style="font-size:14px;color:var(--default-text-color)">
                        <h2 style="margin-top: 0">${"Something went wrong"}</h2>
                        ${"Some tweets couldn't be loaded due to errors."}<br>
                        ${"Please copy text below and send it to $AT1$issue tracker$AT2$ or $AT3$my email$AT2$. Thank you!".replace('$AT1$', "<a target='_blank' href='https://github.com/dimdenGD/YeahTwitter/issues'>").replace(/\$AT2\$/g, '</a>').replace("$AT3$", "<a target='_blank' href='mailto:[email protected]'>")}
                    </span>
                    <div class="box" style="font-family:monospace;line-break: anywhere;padding:5px;margin-top:5px;background:rgba(255, 0, 0, 0.1);color:#ff4545">
                        ${escapeHTML(e.stack ? e.stack : String(e))} at ${t.id_str} (YeahTwitter v${chrome.runtime.getManifest().version})
                    </div>
                </div>
            `);
        }
        return null;
    }
}

// scripts/content.js
const API_URL = `https://yeah.dimden.dev/api`;

Promise.all([
    GM_fetch('https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/styles/style.css').then(res => res.text()),
    GM_fetch('https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/styles/tweet.css').then(res => res.text())
]).then(styles => {
    setTimeout(() => {
        for(let css of styles) {
            let style = document.createElement('style');
            let head = document.head || document.getElementsByTagName('head')[0];
            let isFirefox = navigator.userAgent.indexOf('Firefox') > -1;
            if(isFirefox) css = css.replaceAll('chrome-extension://', 'moz-extension://');
            style.innerHTML = css.replaceAll('__MSG_@@extension_id__', chrome.runtime.id);
            head.appendChild(style);
        }
    }, 750);
});

setTimeout(async () => {
    let yeahToken = await getYeahToken();
    let ignorePopup = await new Promise(resolve => chrome.storage.local.get('ignorePopup', result => resolve(result.ignorePopup)));
    let userId = await getUserId();
    if(!yeahToken && ignorePopup && ignorePopup[userId]) {
        return;
    }
    if(!yeahToken) {
            let modalOpenTime = Date.now();
            let modal = createModal(/*html*/`
                <h2 style="margin-top:0">
                    <img src="${'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_on32.png'}" alt="Yeah!" style="width: 24px; height: 24px;margin-bottom: -4px;">
                    Welcome to Yeah! for Twitter extension!
                </h2>
                <p>This extension adds a <b>Yeah!</b> button to all tweets, which is essentially same thing as a Like but public to everyone. Everyone can see who Yeahed a tweet, and everyone can see all your Yeahs on your profile.</p>
                <p>It doesn't send a spammy reply with an image, instead it saves your Yeahs into a shared database.</p>
                <p>
                    In order to get started, you need to authenticate your Twitter account.
                    Click button below, and we'll automatically post a tweet on your behalf that will look like 'yeah-xxxxxxxx'.
                    Then our server will check for that tweet existence, confirm that it's you, and extension will automatically remove the tweet and save your token.
                    This tweet should be only up for about a second, so don't worry about posting nonsensical tweet.
                </p>
                <p>
                    <b>Important: your account must not be private so server can actually see the tweet. You'll need to make your account public for this auth, afterwards you can make it private again.</b>
                </p>
                <div class="error-message"></div>
                <div>
                    <button class="auth-button nice-yeah-button">Authenticate</button>
                </div>
                <div style="margin-top: 10px">
                    <span class="subtle dontshow" role="button">Never show this popup for this account</span>
                </div>
            `, 'welcome-modal', () => {}, () => Date.now() - modalOpenTime > 1250);
        
            let button = modal.querySelector('.auth-button');
            button.addEventListener('click', async () => {
                button.disabled = true;
                button.textContent = 'Authenticating...';
                let tweetId;
                try {
                    // get tokens
                    let tokens = JSON.parse(await callYeahApi('/request_token'));
        
                    // create tweet
                    let tweet = await callTwitterApi('POST', '/graphql/oB-5XsHNAbjvARJEc8CZFw/CreateTweet', {}, {
                        "variables":{"tweet_text": `yeah-${tokens.public_token}`,"dark_request":false,"media":{"media_entities":[],"possibly_sensitive":false},"semantic_annotation_ids":[]},
                        "features":{"communities_web_enable_tweet_community_results_fetch":true,"c9s_tweet_anatomy_moderator_badge_enabled":true,"tweetypie_unmention_optimization_enabled":true,"responsive_web_edit_tweet_api_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"view_counts_everywhere_api_enabled":true,"longform_notetweets_consumption_enabled":true,"responsive_web_twitter_article_tweet_consumption_enabled":true,"tweet_awards_web_tipping_enabled":false,"creator_subscriptions_quote_tweet_preview_enabled":false,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_inline_media_enabled":true,"articles_preview_enabled":true,"rweb_video_timestamps_enabled":true,"rweb_tipjar_consumption_enabled":true,"responsive_web_graphql_exclude_directive_enabled":true,"verified_phone_label_enabled":false,"freedom_of_speech_not_reach_fetch_enabled":true,"standardized_nudges_misinfo":true,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_enhance_cards_enabled":false},
                        "queryId":"oB-5XsHNAbjvARJEc8CZFw"
                    });
        
                    // parse tweet
                    let tweetResult = tweet.data.create_tweet.tweet_results.result;
                    let tweetData = tweetResult.legacy;
                    tweetData.user = tweetResult.core.user_results.result.legacy;
                    tweetData.user.id_str = tweetData.user_id_str;
                    tweetId = tweetData.id_str;
        
                    // send tweet
                    let res = await callYeahApi('/verify_token', {
                        tweet: tweetData,
                        public_token: tokens.public_token,
                        private_token: tokens.private_token
                    });
                    if(res === 'success') {
                        chrome.storage.local.get('yeahTokens', result => {
                            if(!result.yeahTokens) result.yeahTokens = {};
                            result.yeahTokens[userId] = tokens.private_token;
                            chrome.storage.local.set(result);
                        });
                        modal.removeModal();
        
                        modalOpenTime = Date.now();
                        let modal2 = createModal(/*html*/`
                            <h2 style="margin-top:0">
                                <img src="${'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_on32.png'}" alt="Yeah!" style="width: 24px; height: 24px;margin-bottom: -4px;">
                                Authentification successful!
                            </h2>
                            <p>You can now Yeah! on any tweet. Yeah!!!!!</p>
                            <div>
                                btw I (<a href="/d1mden" target="_blank" style="text-decoration:none;color:#1d9bf0">@d1mden</a>) make a lot of cool extensions for Twitter like this, maybe u wanna follow me?
                            </div>
                            <div style="margin-top: 10px;"><button class="follow-button nice-yeah-button">Yeah! (Follow)</button></div>
                        `, 'authentification-successful', () => {}, () => Date.now() - modalOpenTime > 1500);
        
                        let followButton = modal2.querySelector('.follow-button');
                        followButton.addEventListener('click', () => {
                            callTwitterApi('POST', '/1.1/friendships/create.json', {
                                "Content-Type": "application/x-www-form-urlencoded"
                            }, {
                                include_profile_interstitial_type: 1,
                                include_blocking: 1,
                                include_blocked_by: 1,
                                include_followed_by: 1,
                                include_want_retweets: 1,
                                include_mute_edge: 1,
                                include_can_dm: 1,
                                include_can_media_tag: 1,
                                include_ext_is_blue_verified: 1,
                                include_ext_verified_type: 1,
                                include_ext_profile_image_shape: 1,
                                skip_status: 1,
                                user_id: "1708130407663759360"
                            }).then(() => {
                                modal2.removeModal();
                                alert('Thank you! Happy Yeahing!');
                            }).catch(e => {
                                console.error(e);
                                location.href = '/d1mden';
                            });
                        });
                    } else {
                        throw new Error(res);
                    }
                } catch(e) {
                    console.error(e);
                    modal.querySelector('.error-message').innerHTML = `Failed to authenticate. Please try again later. (${e.message})`;
                } finally {
                    button.disabled = false;
                    button.textContent = 'Authenticate';
                    if(tweetId) {
                        callTwitterApi('POST', `/graphql/VaenaVgh5q5ih7kvyVjgtg/DeleteTweet`, {}, {
                            variables: {tweet_id: tweetId, dark_request: false},
                            queryId: "VaenaVgh5q5ih7kvyVjgtg"
                        });
                    }
                }
            });

            let dontshow  = modal.querySelector('.dontshow');
            dontshow.addEventListener('click', () => {
                chrome.storage.local.get('ignorePopup', result => {
                    if(!result.ignorePopup) result.ignorePopup = {};
                    result.ignorePopup[userId] = true;
                    chrome.storage.local.set(result);
                    modal.removeModal();
                    alert('Popup will not show again for this account. If you want to show it again, press on extension icon and press "Reset popup settings".');
                });
            });
        };
}, 1000);

let fetchQueue = [];
function hookIntoTweets() {
    let tweets = document.getElementsByTagName('article');

    for (let i = 0; i < tweets.length; i++) {
        let tweet = tweets[i];
        if(tweet.dataset.yeahed) continue;
        tweet.dataset.yeahed = true;

        let linkToTweet = Array.from(tweet.querySelectorAll('a[role="link"]')).find(a => a.href.includes('/status/') && !a.href.includes('/photo') && !a.href.includes('/video'));
        let oldTwitter = false;
        if(!linkToTweet) {
            let tweetDiv = tweet.closest('.tweet, .yeah-tweet');
            if(tweetDiv) {
                oldTwitter = true;
                linkToTweet = tweetDiv.querySelector('.tweet-time');
            } else {
                continue;
            }
        };
        let id = linkToTweet.href.match(/\/status\/(\d+)/)[1];
        if(!id) continue;

        fetchQueue.push(id);

        let div = document.createElement('div');
        let button = document.createElement('button');
        button.dataset.count = tweetCache[id] ? tweetCache[id].count : 0;
        button.addEventListener('click', async () => {
            if(!await getYeahToken()) {
                return alert('You need to authenticate first (refresh page for auth popup to appear)');
            }
            if(!button.classList.contains('yeahed')) {
                callYeahApi('/yeah', {
                    post_id: id
                });
                button.querySelector('.yeah-image').src = 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_on32.png';
                let yeahCounter = button.querySelector('.yeah-counter');
                let count = parseInt(button.dataset.count);
                yeahCounter.innerText = formatLargeNumber(count + 1);
                button.dataset.count = count + 1;
                button.classList.add('yeahed');
                if(tweetCache[id]) {
                    tweetCache[id].yeahed = true;
                    tweetCache[id].count++;
                }
                let likeButton = tweet.querySelector('button[data-testid="like"], .tweet-interact-favorite:not(.tweet-interact-favorited)');
                if(likeButton) {
                    let settings = await getYeahSettings();
                    if(!settings.dontLike) likeButton.click();
                }
            } else {
                callYeahApi('/unyeah', {
                    post_id: id
                });
                button.classList.remove('yeahed');
                let yeahCounter = button.querySelector('.yeah-counter');
                let count = parseInt(button.dataset.count);
                yeahCounter.innerText = formatLargeNumber(count - 1);
                button.dataset.count = count - 1;
                if(count - 1 <= 0) yeahCounter.innerText = '';
                if(tweetCache[id]) {
                    tweetCache[id].yeahed = false;
                    tweetCache[id].count--;
                    if(tweetCache[id].count < 0) tweetCache[id].count = 0;
                }
                let likeButton = tweet.querySelector('button[data-testid="unlike"], .tweet-interact-favorite.tweet-interact-favorited');
                if(likeButton) {
                    let settings = await getYeahSettings();
                    if(!settings.dontLike) likeButton.click();
                }
            }
        });
        button.addEventListener('mouseover', () => {
            button.querySelector('.yeah-image').src = 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_on32.png';
        });
        button.addEventListener('mouseout', () => {
            if(!button.classList.contains('yeahed')) button.querySelector('.yeah-image').src = 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_off32.png';
        });
        button.className = `yeah-button yeah-button-${id}`;
        div.className = 'yeah-button-container';

        let img = document.createElement('img');
        img.src = tweetCache[id] && tweetCache[id].yeahed ? 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_on32.png' : 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_off32.png';
        if(tweetCache[id] && tweetCache[id].yeahed) button.classList.add('yeahed');
        img.className = 'yeah-image';
        img.draggable = false;
        button.appendChild(img);

        let counter = document.createElement('span');
        counter.className = 'yeah-counter';
        counter.innerText = tweetCache[id] && typeof tweetCache[id].count === 'number' ? formatLargeNumber(tweetCache[id].count) : '';
        if(oldTwitter) {
            counter.classList.add('yeah-counter-oldtwitter');
        }
        
        button.appendChild(counter);
        div.appendChild(button);

        let group = tweet.querySelector('div[role="group"]');
        if(group && group.children && group.children[3]) group.children[3].after(div);
        else {
            let interactButton = tweet.querySelector('.tweet-interact-favorite, .tweet-yeah-interact-favorite');
            if(interactButton) {
                div.classList.add('yeah-button-container-oldtwitter');
                interactButton.after(div);
            }
        }
    }
}

function updateButton(data) {
    if(!data) return;
    let buttons = Array.from(document.getElementsByClassName(`yeah-button-${data.post_id}`));
    for(let button of buttons) {
        if(data.yeahed) {
            button.classList.add('yeahed');
            button.querySelector('.yeah-image').src = 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_on32.png';
        } else {
            button.classList.remove('yeahed');
            button.querySelector('.yeah-image').src = 'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/yeah_off32.png';
        }
        button.dataset.count = data.count;

        let counter = button.querySelector('.yeah-counter');
        counter.innerText = data.count === 0 ? '' : formatLargeNumber(data.count);
    }
}

let tweetCache = {};
setInterval(() => tweetCache = {}, 1000 * 60 * 5);
setInterval(async () => {
    if(fetchQueue.length > 0 && await getYeahToken()) {
        let first100 = fetchQueue.splice(0, 100);
        let cachedData = first100.map(id => tweetCache[id]).filter(Boolean);
        for(let cache of cachedData) {
            updateButton(cache);
        }
        first100 = first100.filter((id) => !tweetCache[id]);
        if(!first100.length) return;
        for(let id of first100) {
            tweetCache[id] = {
                post_id: id,
                yeahed: false,
                count: 0
            };
        }
        let data = JSON.parse(await callYeahApi('/get', {
            post_ids: first100.join(',')
        }));
        for(let i in data) {
            tweetCache[data[i].post_id] = data[i];
            updateButton(data[i]);
        }
    }
}, 1500);

function hookIntoInteractions() {
    let path = window.location.pathname;
    let addedTab;
    if(path.includes('/status/') && (path.endsWith('/quotes') || path.endsWith('/retweets') || path.endsWith('/likes'))) {
        let tablist = document.querySelector('div[role="tablist"]');
        if(!tablist) return;
        if(tablist.dataset.yeahed) return;
        tablist.dataset.yeahed = true;

        let yeahTab = document.createElement('div');
        yeahTab.className = 'yeah-tab';

        let span = document.createElement('span');
        span.innerText = 'Yeahs';
        yeahTab.appendChild(span);
        tablist.appendChild(yeahTab);

        addedTab = yeahTab;
    } else {
        let tablist = document.querySelector('.tweet-footer-stats');
        if(!tablist) return;
        if(tablist.dataset.yeahed) return;
        tablist.dataset.yeahed = true;

        let yeahTab = document.createElement('a');
        yeahTab.className = 'tweet-footer-stat';
        yeahTab.style.cursor = 'pointer';

        let span = document.createElement('span');
        span.innerText = 'Yeahs';
        span.className = 'tweet-footer-stat-text';

        let b = document.createElement('b');
        let id = location.pathname.match(/\/status\/(\d+)/)[1];
        b.innerText = tweetCache[id] && typeof tweetCache[id].count === 'number' ? formatLargeNumber(tweetCache[id].count) : '?';
        b.className = 'tweet-footer-stat-count';

        yeahTab.appendChild(span);
        yeahTab.appendChild(b);
        tablist.appendChild(yeahTab);

        addedTab = yeahTab;
    }

    if(addedTab) {
        addedTab.addEventListener('click', async() => {
            if(!await getYeahToken()) {
                return alert('You need to authenticate first (refresh page for auth popup to appear)');
            }
            let modal = createModal(/*html*/`
                <h3>Yeahs</h3>
                <div class="list"></div>
                <div class="loader" style="text-align:center">
                    <img src="${'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/loading.svg'}" width="64" height="64">
                </div>
            `, 'yeah-users');

            let list = modal.querySelector('.list');

            let data = JSON.parse(await callYeahApi('/get_users', {
                post_id: path.match(/\/status\/(\d+)/)[1],
                page: 1
            }));

            if(!data.length) {
                modal.querySelector('.loader').hidden = true;
                list.innerHTML = 'No Yeahs yet';
                return;
            }

            let lookup = await API.user.lookup(data);

            modal.querySelector('.loader').hidden = true;

            let addedUsers = [];
            
            for(let id of data) {
                let user = lookup.find(user => user.id_str === id);
                if(user) {
                    appendUser(user, list);
                    addedUsers.push(user.id_str);
                }
            }

            let modalContent = modal.querySelector('.yeah-modal-content');
            let over = false, loadingMore = false, page = 2;
            modalContent.addEventListener('scroll', async () => {
                if(over) return;
                if(loadingMore) return;

                let scrollPosition = modalContent.scrollTop + modalContent.offsetHeight;
                if(scrollPosition >= modalContent.scrollHeight - 200) {
                    loadingMore = true;
                    modal.querySelector('.loader').hidden = false;
                    let data = JSON.parse(await callYeahApi('/get_users', {
                        post_id: path.match(/\/status\/(\d+)/)[1],
                        page: page++
                    }));
                    if(!data.length) {
                        over = true;
                        modal.querySelector('.loader').hidden = true;
                        return;
                    }
                    let lookup = await API.user.lookup(data);
                    for(let id of data) {
                        if(addedUsers.includes(id)) continue;

                        let user = lookup.find(user => user.id_str === id);
                        if(user) {
                            appendUser(user, list);
                            addedUsers.push(user.id_str);
                        }
                    }
                    loadingMore = false;
                    modal.querySelector('.loader').hidden = true;
                }
            });
        });
    }
}

function hookIntoProfile() {
    if(['/notifications', '/explore', '/home', '/messages'].includes(window.location.pathname)) return;
    if(window.location.pathname.startsWith('/search')) return;
    if(window.location.pathname.startsWith('/i/')) return;
    if(window.location.pathname.startsWith('/explore/')) return;
    if(window.location.pathname.startsWith('/notifications/')) return;
    if(window.location.pathname.startsWith('/messages/')) return;
    if(window.location.pathname.includes('/communities/')) return;

    let addedTab;
    let profileStats = document.querySelector('#profile-stats');
    if(!profileStats) {
        let tablist = document.querySelector('div:not([data-testid="toolBar"]) > nav[role="navigation"][aria-live="polite"] div div[role="tablist"]');
        if(!tablist) return;
        if(tablist.dataset.yeahed) return;
        tablist.dataset.yeahed = true;
    
        let yeahTab = document.createElement('div');
        yeahTab.className = 'yeah-tab';
        let span = document.createElement('span');
        span.innerText = 'Yeahs';

        yeahTab.appendChild(span);   
        tablist.appendChild(yeahTab);
        
        addedTab = yeahTab;
    } else {
        if(profileStats.dataset.yeahed) return;
        profileStats.dataset.yeahed = true;

        let yeahTab = document.createElement('a');
        yeahTab.className = 'profile-stat';
        yeahTab.style.cursor = 'pointer';

        let span = document.createElement('span');
        span.innerText = 'Yeahs';
        span.className = 'profile-stat-text';

        let span2 = document.createElement('span');
        span2.className = 'profile-stat-value';
        span2.innerText = '?';

        setTimeout(() => {
            let avatar = document.getElementById('profile-avatar');
            if(!avatar || !avatar.dataset.user_id) return;
            let id = avatar.dataset.user_id;
            callYeahApi('/get_user_yeah_count', {
                user_id: id
            }).then(data => {
                data = JSON.parse(data);
                if(typeof data.count === 'number') span2.innerText = formatLargeNumber(data.count);
            });
        }, 2000);

        yeahTab.appendChild(span);
        yeahTab.appendChild(span2);

        profileStats.appendChild(yeahTab);

        addedTab = yeahTab;
    }
    if(addedTab) addedTab.addEventListener('click', async () => {
        if(!await getYeahToken()) {
            return alert('You need to authenticate first (refresh page for auth popup to appear)');
        }
        let username = window.location.pathname.split('/')[1];
        let modal = createModal(/*html*/`
            <h3>${username}'s Yeahs</h3>
            <div class="list"></div>
            <div class="loader" style="text-align:center">
                <img src="${'https://raw.githubusercontent.com/dimdenGD/YeahTwitter/main/images/loading.svg'}" width="64" height="64">
            </div>
        `, 'yeah-posts');

        let list = modal.querySelector('.list');
        let user = await API.user.get(username, false);

        let data = JSON.parse(await callYeahApi('/get_yeahs', {
            user_id: user.id_str,
            page: 1
        }));

        if(!data.length) {
            modal.querySelector('.loader').hidden = true;
            list.innerHTML = 'No Yeahs yet';
            return;
        }

        let tweets = await API.tweet.lookup(data);

        if(!tweets.length) {
            modal.querySelector('.loader').hidden = true;
            list.innerHTML = 'No Yeahs yet';
            return;
        }

        
        let addedPosts = [];
        for(let id of data) {
            let tweet = tweets.find(tweet => tweet.id_str === id);
            if(tweet) {
                appendTweet(tweet, list, {}, user);
                addedPosts.push(tweet.id_str);
            }
        }
        modal.querySelector('.loader').hidden = true;

        let modalContent = modal.querySelector('.yeah-modal-content');
        let over = false, loadingMore = false, page = 2;
        modalContent.addEventListener('scroll', async () => {
            if(over) return;
            if(loadingMore) return;

            let scrollPosition = modalContent.scrollTop + modalContent.offsetHeight;
            if(scrollPosition >= modalContent.scrollHeight - 200) {
                loadingMore = true;
                modal.querySelector('.loader').hidden = false;
                let data = JSON.parse(await callYeahApi('/get_yeahs', {
                    user_id: user.id_str,
                    page: page++
                }));
                if(!data.length) {
                    over = true;
                    modal.querySelector('.loader').hidden = true;
                    return;
                }
                let tweets = await API.tweet.lookup(data);
                for(let id of data) {
                    if(addedPosts.includes(id)) continue;
                    let tweet = tweets.find(tweet => tweet.id_str === id);
                    if(tweet) {
                        appendTweet(tweet, list, {}, user);
                        addedPosts.push(tweet.id_str);
                    }
                }
                loadingMore = false;
                modal.querySelector('.loader').hidden = true;
            }
        });
    });
}

setInterval(hookIntoTweets, 250);
setInterval(hookIntoInteractions, 500);
setInterval(hookIntoProfile, 500);