import Tracker from '../Tracker'; export default class YouTubeTracker extends Tracker{ constructor(worker, privacy, extensionfilter=[]){ super(worker, privacy); this.extensionfilter = extensionfilter; this.onStart = this.onStart.bind(this); this.rootElement = null; this.fetchCategorie = { is: false, count: 0, MAX_COUNT: 4 }; this.allow = true; this.youtube_debug = false; this.eventElements = { root: ['#primary'], allow: ['#primary'], logged_email: ['yt-formatted-string#email', '.yt-masthead-picker-header'], logged_fullname: ['yt-formatted-string#account-name', '.yt-masthead-picker-name'], profile_pic_url_comment: 'yt-img-shadow#author-thumbnail img#img', profile_pic_url_avatar: ['#avatar-btn img#img', '.yt-masthead-user-icon img'], watch_later: '.ytp-watch-later-button.ytp-button', svg_unlisted: '#container .style-scope.ytd-badge-supported-renderer svg g path[d="M3.9 12c0-1.71 1.39-3.1 3.1-3.1h4V7H7c-2.76 0-5 2.24-5 5s2.24 5 5 5h4v-1.9H7c-1.71 0-3.1-1.39-3.1-3.1zM8 13h8v-2H8v2zm9-6h-4v1.9h4c1.71 0 3.1 1.39 3.1 3.1s-1.39 3.1-3.1 3.1h-4V17h4c2.76 0 5-2.24 5-5s-2.24-5-5-5z"]', //svg_private: '#container .style-scope.ytd-badge-supported-renderer svg g path[d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"]', categorie: { contents: ['#content', '#collapsible'], button: { more: ['#more'], less: ['#less'] }, categories: ['#container #collapsible *:first-child #content a'] }, likearticelButton: ['#top-level-buttons > *:nth-child(1) a'], dislikearticelButton: ['#top-level-buttons > *:nth-child(2) a'], subscribeButton: ['#subscribe-button [role="button"]'], newComment: ['#simple-box'], hashtag: ['a.yt-simple-endpoint[href]'], commentWrapper: [ { name: 'postanswer', query: ['#comments #contents > *'], }, { name: 'postanswer-comment', query: ['#expander-contents #loaded-replies > *'], } ], comment: { content: ['#content-text'], timeContent: ['#header-author .published-time-text'], buttonLike: ['#toolbar #like-button a'], buttonDislike: ['#toolbar #dislike-button a'], countMiddle: ['#toolbar #vote-count-middle'], countComments: ['#expander #more #more-text'], moreCommentFromComments: [ { parentNode: ['#replies #expander'], button: ['#more'], content: ['#content'], } ], buttonComment: [ { button: ['#reply-button-end'], parent: '#action-buttons', } ], }, commentButton: { wrapper: ['#comment-dialog', '#reply-dialog'], input: ['#contenteditable-textarea'], submit: ['#submit-button'] } }; this.logged_email = null; this.is_correct_logged_fullname = false; this.logged_fullname = null; this.is_correct_profile_pic_url = false; this.profile_pic_url = null; this.updateMetada = false; this.privacy_flags = { 'email': null, 'fullname': null, 'guest_id': null, 'private': null } this.lastUrlPath = ''; this.values = []; this.startswith_denylist = ['/account/', '/reporthistory/', '/upload/', '/account_notifications/', '/account_playback/', '/account_privacy/', '/account_sharing/', '/pair/', '/account_billing/', '/account_advanced/']; } /** * [_getValues return values of articel] * @param {Object} target * @return {Array} */ _getValues(target){ let search = [ { name: 'articel-time', query: ['.date'], default: undefined, filter: e => e.textContent.match(/\b(\w*[A-Za-z0-9 äÄöÖüÜß]\w*)\b/g).join('') }, { name: 'articel-link', query: ['#primary-inner'], default: undefined, filter: e => location.href }, { name: 'articel-headertext', query: ['.title'], default: undefined, filter: e => e.textContent }, { name: 'articel-description', query: ['#meta #container #content #description'], default: undefined, filter: e => e.textContent }, { name: 'articel-publisher-name', query: ['#owner-name a'], default: undefined, filter: e => e.textContent }, { name: 'articel-count-likes', query: ['#top-level-buttons > *:nth-child(1) #text'], default: undefined, filter: e => e.textContent }, { name: 'articel-count-dislikes', query: ['#top-level-buttons > *:nth-child(2) #text'], default: undefined, filter: e => e.textContent }, { name: 'articel-count-comments', query: ['#comments #count'], default: undefined, filter: e => parseInt(e.textContent.replace(/\D+/g, ""), 10) } ], values= []; if(search.length == this.values.length) return this.values let is = this.values.filter(e => e.value!=undefined).map(e => e.name); for (let s of search) { if(is.includes(s.name)) continue; try { let value = s.default; for (let query of s.query) { var r = this._getRootElement().querySelectorAll(query); if(r.length>0){ r = r[0]; let data = s.filter(r); if(data!=null) value = data; } }//for // console.log(s.name, r, value); this.values.push({name: s.name, value: value}) } catch (err) { console.log(err); console.log(target, err.toString()); } }//for return this.values; } /** * [_setAllow check if url changed and search in dom if find some elements they not allowed and set this.allow] */ _isAllow(){ return new Promise((resolve, reject) => { if(this.lastUrlPath!==location.pathname){ this.lastUrlPath = location.pathname; for (let query of this.eventElements.allow){ let found = document.querySelectorAll(query+':not(.tracked)'); this.allow = found.length>0; } } resolve(this.allow) }); } /** * Setup the credentials for the logged user (if any). Not posible in youtube */ reset_credentials(){ this.rootElement = this._getRootElement(); this.is_logged_in = this._isLogged(); if (this.is_logged_in){ this.updateMetada = false; this.logged_email = this._get_logged_email(); this.logged_fullname = this._get_logged_fullname(); this.profile_pic_url = this._get_profile_pic_url(); if (this.updateMetada){ this.fetchMetaData(); } } //document.querySelector('#avatar-btn'); this.is_content_allowed = this.get_is_content_allowed(); // is social media path allowed this.is_sm_path_allowed = this.get_is_sm_path_allowed(location.pathname); } /** * get the metadata from the file * @return {object} the metadata of the html */ getMetadata(){ let metadata = super.getMetadata(); let anonym = {}; if (this.logged_email) { anonym['email'] = this.logged_email; this.privacy_flags['email'] = true; } if (this.logged_fullname) { anonym['fullname'] = this.logged_fullname; this.privacy_flags['fullname'] = true; } if (this.profile_pic_url) { anonym['guest_id'] = this.profile_pic_url; this.privacy_flags['guest_id'] = true; } metadata['anonym'] = anonym; metadata['privacy_flags'] = this.privacy_flags; return metadata; } _get_logged_email(){ if (this.logged_email){ return this.logged_email; } for (let selector of this.eventElements.logged_email) { let el = document.querySelector(selector); if (el){ this.update_metadata = true; return el.textContent; } } return null; } _get_logged_fullname(){ if (this.is_correct_logged_fullname){ return this.logged_fullname; } for (let selector of this.eventElements.logged_fullname) { let el = document.querySelector(selector); if (el){ this.is_correct_logged_fullname = true; this.updateMetada = true; return el.textContent; } } let el = document.querySelector(this.eventElements.profile_pic_url_comment); if (el){ this.updateMetada = true; this.is_correct_logged_fullname = true; return el.alt; } el = document.querySelector(this.eventElements.watch_later); if (el) { return el.title; } return null; } _get_profile_pic_url(){ if (this.is_correct_profile_pic_url){ return this.profile_pic_url; } let src = null; let el = document.querySelector(this.eventElements.profile_pic_url_comment); if (el){ this.updateMetada = true; this.is_correct_profile_pic_url = true; src = el.src; } else { for (let selector of this.eventElements.profile_pic_url_avatar) { el = document.querySelector(selector); if (el){ src = el.src; break; } } } if (src) { let parts = src.split('/'); return parts[parts.length - 1]; } return null; } _isLogged() { if (document.querySelector('#avatar-btn')){ return true; // e.g. used in youtube.com/pair/ } else if (document.querySelector('.yt-masthead-user-icon')) { return true; } return false; } get_is_content_allowed() { if (this.rootElement.querySelector(this.eventElements.svg_unlisted)){ this.privacy_flags['private'] = true; return false; } return true; } /** * [_getRootElement return the rootElement from document] * @return {Object} */ _getRootElement(){ if(this.rootElement == null){ let target = this._getElements(this.eventElements.root, document); if(target.length>0) { return target[0]; } else { return document; } } return this.rootElement; } /** * [_setCategorie2Meta set the categorie to the meta keysworts] */ _setCategorie2Meta(){ if(this.fetchCategorie.is || this.fetchCategorie.count >= this.fetchCategorie.MAX_COUNT) return this.fetchCategorie.count += 1; // console.log('this.fetchCategorie.count', this.fetchCategorie.count); var contents = this._getElements(this.eventElements.categorie.contents, this._getRootElement(), {setTracked: false, color: 'purple'}); for (let content of contents) content.style.display = 'none'; let more = this._getElements(this.eventElements.categorie.button.more, this._getRootElement(), {setTracked: false, color: 'purple'}); // console.log('more', more.length, more); for (let button of more) button.click(); let categories = this._getElements(this.eventElements.categorie.categories, this._getRootElement(), {setTracked: false, color: 'purple'}); // console.log(categories); if(categories.length>0) this.fetchCategorie.is = true; for (let categorie of categories){ this.updateMetaData({keywords: categorie.textContent}); } let less = this._getElements(this.eventElements.categorie.button.less, this._getRootElement(), {setTracked: false, color: 'purple'}); // console.log('less', less.length, less); for (let button of less) button.click(); for (let content of contents) content.style.display = ''; } /** * [_eventSetLike set event for all button likes] */ _eventSetLike(){ let buttons = this._getElements(this.eventElements.likearticelButton, this._getRootElement()); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', e => { this.eventFn.onEvent( { event: 'like', type: 'articel', values: this._getValues().concat([ {name: 'like', value: 'like'}, ]) } ) }); } } /** * [_eventSetLike set event for all button for dislikes] */ _eventSetDislike(){ let buttons = this._getElements(this.eventElements.dislikearticelButton, this._getRootElement()); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', e => { this.eventFn.onEvent( { event: 'like', type: 'articel', values: this._getValues().concat([ {name: 'like', value: 'dislike'}, ]) } ) }); } } /** * [_eventSubscribe subscribe channel event] */ _eventSubscribe(){ let buttons = this._getElements(this.eventElements.subscribeButton, this._getRootElement(), {color: 'blue'}); for (var i = 0; i < buttons.length; i++) { buttons[i].addEventListener('click', e => { this.eventFn.onEvent( { event: 'subscribe', type: 'articel', values: this._getValues() } ) }); } } /** * [_eventNewComment fire event if write new comment] */ _eventNewComment(){ let inputs = this._getElements(this.eventElements.newComment, this._getRootElement(), {color: 'blue'}); for (let input of inputs) { input.addEventListener('click', e => setTimeout(() => { this._eventCommentDialog(input, comment => { this.eventFn.onEvent({ event: 'comment', type: 'articel', values: this._getValues().concat([ {name: 'comment', value: comment}, ]) }) }) }, 500)); } } /** * [_eventClickHashtag fire event if click user on hashtag link] */ _eventClickHashtag(){ let inputs = this._getElements(this.eventElements.hashtag, this._getRootElement(), {color: 'blue'}); for (var i = 0; i < inputs.length; i++) { if(inputs[i].textContent.substring(0, 1)!='#'){ inputs[i].classList.remove('tracked'); this._setBorder(inputs[i], 'transparent'); }else{ inputs[i].addEventListener('click', e => this.eventFn.onEvent( { event: 'hashtag', type: 'articel', values: this._getValues().concat([ {name: 'name', value: e.srcElement.textContent}, ]) } )); } } } /** * [_getValuesFromComment return search values of comment] * @param {Object} target [DomElement] * @param {Object} s [index of this.eventElements.comment array] * @return {Object} [{time: String, content: String, countMiddle: Number}] */ _getValuesFromComment(target){ //----content--- let contentString = null; let content = this._getElements(this.eventElements.comment.content, target, {ignoreTracked: false}); for (let c = 0; c < content.length; c++) contentString = content[c].textContent; //----time comment--- let timeString = null; let timeContent = this._getElements(this.eventElements.comment.timeContent, target, {ignoreTracked: false}); for (let t = 0; t < timeContent.length; t++) timeString = timeContent[t].textContent; //----countMiddle--- let countMiddleInt = null; let countMiddle = this._getElements(this.eventElements.comment.countMiddle, target, {ignoreTracked: false}); for (let cm = 0; cm < countMiddle.length; cm++) countMiddleInt = parseInt(countMiddle[cm].textContent.replace(/\D+/g, ""), 10); return {time: timeString, content: contentString, countMiddle: countMiddleInt}; } /** * [_eventCommentLike set events of like oder dislike comment or write new comment for comment] * @param {Object} target [DomElement default: this.getRootElement(] * @param {Function} event [default: ()=>{}] * @param {Object} type [default: this.eventElements.commentWrapper[0]] */ _eventCommentLike(target=this._getRootElement(), event = () => {}, type=this.eventElements.commentWrapper[0]){ let wrappers = this._getElements(type.query, target, {color: 'blue'}); for (let wrapper of wrappers) { if (this.youtube_debug){ if(type.name=='postanswer'){ this._setBorder(wrapper, 'blue'); }else{ this._setBorder(wrapper, 'green'); } } let values = this._getValuesFromComment(wrapper); let buttonLikes = this._getElements(this.eventElements.comment.buttonLike, wrapper); for (let buttonLike of buttonLikes) { buttonLike.addEventListener('click', e => { event({ event: 'like', type: type.name, values: [ {name: 'postanswer-time', value: values.time}, {name: 'postanswer-count-middle', value: values.countMiddle}, {name: 'postanswer-text', value: values.content}, {name: 'like-value', value: 'like'} ] }) }); }//for buttonLikes let buttonDislikes = this._getElements(this.eventElements.comment.buttonDislike, wrapper); for (let buttonDislike of buttonDislikes) { buttonDislike.addEventListener('click', e => { event({ event: 'like', type: type.name, values: [ {name: 'postanswer-time', value: values.time}, {name: 'postanswer-count-middle', value: values.countMiddle}, {name: 'postanswer-text', value: values.content}, {name: 'like-value', value: 'dislike'} ] }) }); }//for buttonLikes for (let buttonComment of this.eventElements.comment.buttonComment) { let buttonC = this._getElements(buttonComment.button, wrapper); for (let button of buttonC) { button.addEventListener('click', e => setTimeout(() => { try { let actionButtons = this.getParentElement(e.srcElement, buttonComment.parent); this._eventCommentDialog(actionButtons, comment => { event({ event: 'like', type: type.name, values: [ {name: 'postanswer-time', value: values.time}, {name: 'postanswer-count-middle', value: values.countMiddle}, {name: 'postanswer-text', value: values.content}, {name: 'comment', value: comment} ] }); }); } catch (e) { console.log(e); } }, 500)); } }//for buttonComment }//for wrappers wrappers = this._getElements(type.query, target, {filter: '.tracked:not(.find-replies)', addClass: ''}); for (let wrapper of wrappers) { for (let b of this.eventElements.comment.moreCommentFromComments) { let parentNodes = this._getElements(b.parentNode, wrapper, {color: 'blue'}); let values = this._getValuesFromComment(wrapper); for (let node of parentNodes) { wrapper.classList.add('find-replies'); let content = this._getElements(b.content, node)[0]; let timeout = null if (content){ content.addEventListener('DOMSubtreeModified', () => { if(typeof timeout == 'number') clearTimeout(timeout); timeout = setTimeout(() => this._eventCommentLike(content, e => { let returnEvent = { event: 'like', type: e.type, values: [ {name: 'postanswer-time', value: values.time}, {name: 'postanswer-count-middle', value: values.countMiddle}, {name: 'postanswer-text', value: values.content}, ] } for (let v of e.values) { returnEvent.values.push({ name: e.type+'_'+v.name, value: v.value }) } event(returnEvent); }, this.eventElements.commentWrapper[1]), 500); }); } }//for parentNodes }//for moreCommentFromComments }//for }//() /** * [_eventCommentDialog eventhandling for wirting new comment] * @param {Object} target [description] * @param {Function} fn [default: ()=>{}] */ _eventCommentDialog(target, fn = ()=> {}){ let wrappers = this._getElements(this.eventElements.commentButton.wrapper, target, {color: 'green'}); for (let wrapper of wrappers) { let comment = ''; let inputs = this._getElements(this.eventElements.commentButton.input, wrapper); for (let input of inputs){ input.addEventListener('keyup', e => { comment = e.srcElement.textContent; }) } let submits = this._getElements(this.eventElements.commentButton.submit, wrapper); for (let submit of submits){ submit.addEventListener('click', e => { if(comment.length>0) fn(comment) }) } }//for } /** * [return element without embedd js, css, etc] * @return {Promise} */ _clean_embedded_scripts(target, selectors='script:not([src]),svg,style'){ return super._clean_embedded_scripts(target, selectors + ',dom-module'); } /** * [getDom return html content from public articel] * @return {String} */ getDom(){ return new Promise((resolve, reject) => { // if(this._isAllow()){ //this._setCategorie2Meta(); // this._eventSetLike(); // this._eventSetDislike(); // this._eventSubscribe(); // this._eventNewComment(); // setTimeout(()=>{ // this._eventCommentLike(undefined, e => { // this.eventFn.onEvent( // { // event: e.event, // type: e.type, // values: this._getValues().concat(e.values) // } // ) // }); // this._eventClickHashtag(); // }, 500); // cloning the dom does not work as expected :/ // resolve(this._getDom()); resolve(document.documentElement.outerHTML); // } else { // if (this.youtube_debug) console.log('YouTube Not allow'); // resolve(false) // } }); } /** * [onStart on start event] * @param {Function} fn */ onStart(fn){ setTimeout(() => { if (this.youtube_debug) console.log('START!!!!'); fn(2000); }, 1000); } }//class