🏠 Home 

Instagram Logger

Log stuff on the instagram homepage. It will only log what you see.


Install this script?
// ==UserScript==
// @name         Instagram Logger
// @namespace    https://github.com/wi99
// @version      1.1
// @description  Log stuff on the instagram homepage. It will only log what you see.
// @author       William Situ
// @match        https://www.instagram.com/
// @match        https://www.instagram.com/accounts/activity/
// @include      https://www.instagram.com/stories/*/
// @grant        none
// ==/UserScript==
function parsePost(articElem){
return {'shortcode': articElem.getElementsByTagName('a')[articElem.getElementsByTagName('a').length-1].href.split('/').slice(-2)[0],
'timestamp': Math.floor(Date.parse(articElem.getElementsByTagName('time')[0].attributes.datetime.value) / 1000), // Instagram uses unix epoch time so I will too.
'username': articElem.getElementsByTagName('a')[0].href.split('/').slice(-2)[0]} // normally second <a> is username, but when there's a story first <a> is username. URL works either way.
}
function logPosts(posts_list){
var posts_observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type == 'childList' && mutation.addedNodes[0] && mutation.addedNodes[0].tagName == 'ARTICLE') {
logData('posts', parsePost(mutation.addedNodes[0]));
}
});
});
for (var i=0;i<posts_list.children.length;i++){
if (posts_list.children[i].tagName == 'ARTICLE')
logData('posts', parsePost(posts_list.children[i]));
}
posts_observer.observe(posts_list, {attributes: true, childList: true, characterData: true});
}
function parseStory(divElem){
return {'timestamp': Math.floor(Date.parse(divElem.getElementsByTagName('time')[0].attributes.datetime.value) / 1000),
'username': divElem.getElementsByTagName('span')[1].innerText}
}
function logStories(stories_list){
var stories_observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type == 'childList' && mutation.addedNodes[0]) {
logData('stories', parseStory(mutation.addedNodes[0]));
}
});
});
for (var j=0;j<stories_list.children.length;j++){
logData('stories', parseStory(stories_list.children[j]));
}
stories_observer.observe(stories_list, {attributes: true, childList: true, characterData: true});
}
function parseAction(divElem){
return {'username': divElem.getElementsByTagName('a')[0].href.split('/').slice(-2)[0],
'action': divElem.children[1].childNodes[0].data.trim()+divElem.children[1].childNodes[2].data.trim(), // only "Your facebook friend is on Instagram as" uses childNodes[0]. TODO: decide whether to log that facebook thing
'timestamp': Math.floor(Date.parse(divElem.getElementsByTagName('time')[0].attributes.datetime.value) / 1000)}
}
function logActivity(activity_list) {
var activity_observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type == 'childList' && mutation.addedNodes[0]) {
logData('activity', parseAction(mutation.addedNodes[0]));
}
});
});
for (var i = 0; i < activity_list.children.length; i++) {
logData('activity', parseAction(activity_list.children[i]));
}
activity_observer.observe(activity_list, {attributes: true, childList: true, characterData: true});
}
/* check support for indexedDB */ if (!window.indexedDB) {
window.alert('No support for IndexedDB');
}
/**
* Data to indexedDB
* @param {string} storeName - name of object store in indexedDB to log it in
* @param {JSONobject} item - JSON object to log
*/
function logData(storeName, item){
var request = indexedDB.open('InstagramLog', 1); // starts from 1
request.onerror = function(event) {
alert("Why didn't you allow me to use IndexedDB?!");
};
request.onupgradeneeded = function(event) {
var db = event.target.r###lt;
var objectStore = db.createObjectStore("posts", { keyPath: "shortcode" });
objectStore.createIndex("timestamp", "timestamp");
objectStore.createIndex("username", "username");
/* if(db.objectStoreNames.contains('stories')) { // what if something weird was set as the keypath/index? TODO: if weird then revert to normal. (TODO: fix when browser closes after db open but before objct stores created)
objectStore2 = event.target.transaction.objectStore('stories');
objectStore2.deleteIndex('username');
objectStore2.createIndex('');
} else {*/
var objectStore2 = db.createObjectStore("stories", { keyPath: ['timestamp', 'username']});
objectStore2.createIndex("timestamp", "timestamp");
objectStore2.createIndex("username", "username");
var objectStore3 = db.createObjectStore("activity", { keyPath: ['timestamp', 'username']});
objectStore3.createIndex("timestamp", "timestamp");
objectStore3.createIndex("action", "action");
objectStore3.createIndex("username", "username"); // FYI: only whatever in keypaths are unique
};
request.onsuccess = function(event) {
var db = event.target.r###lt;
var tx = db.transaction(storeName, "readwrite"); // ['posts', 'stories', 'activity'] also works here
var store = tx.objectStore(storeName);
//console.log(JSON.stringify(item))
store.add(item) // no need to overwrite so I use add() instead of put()
tx.onerror = function(){
console.log('tx.onerror') // I wonder if I should handle duplicate entry myself instead of pushing it to error
db.close();
}
tx.oncomplete = function() {
console.log('tx.oncomplete')
db.close();
};
};
}
if (document.documentElement.classList.contains('logged-in')){
// Only mutation observers need to stay in the event listener
window.addEventListener('load', function() {
function homeMode(){
/* Posts Logging*/
try{
if (document.documentElement.classList.contains('touch')){ // mobile
logPosts(document.getElementsByTagName('main')[0].children[0].lastElementChild.children[0].children[0]);
}
else{ // desktop
logPosts(document.getElementsByTagName('main')[0].children[0].children[0].children[0].children[0]);
}
}
catch(e){ // This is not expected to happen
console.log('unexpected' + e)
}
finally{
}
/* Stories Logging */
try{
if (!document.documentElement.classList.contains('touch')){ // only do desktop b/c they don't timestamp on mobile
logStories(document.getElementsByTagName('main')[0].children[0].lastElementChild.children[3].children[0].children[0])
}
}
catch(e){ // on desktop web, stories don't show if window width too small.
// TODO: mutation observer for this instead
setTimeout(logStories, 30000) // maybe doubleing time better
}
finally{
}
/* Activity Logging */
// FYI: followers is in the zip data download but other people's likes is not
try{
if (!document.documentElement.classList.contains('touch')){ // activity is not a drop down for mobile
// activity_list is at
// document.getElementsByClassName('coreSpriteDesktopNavActivity')[0].parentNode.lastElementChild.children[0].lastElementChild.lastElementChild.children[0].children[0]
// before we can logActivity, we have to                                          ^^ wait for this to be created (loading)      ^^ then wait for this to be created
// nested MutationObserver. TODO: make function for all the mutation observers so code is smaller and easier to read.
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.addedNodes[0]) {
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.addedNodes[0]) {
logActivity(mutation.addedNodes[0].children[0].children[0])
}
});
});
observer.observe(mutation.addedNodes[0].children[0].lastElementChild, {childList: true});
}
});
});
observer.observe(document.getElementsByClassName('coreSpriteDesktopNavActivity')[0].parentNode, {childList: true});
}
}
catch(e){ // not expected to happen
console.log('unexpected' + e)
}
finally{
}
}
function storyMode(){
function logStoriesStoryMode(){
return {'timestamp': Math.floor(Date.parse(document.getElementsByTagName('time')[0].attributes.datetime.value) / 1000),
'username': document.getElementsByTagName('a')[0].href.split('/').slice(-2)[0]}
}
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.type == 'childList' && mutation.addedNodes[0]) {
if (window.location.pathname.split('/')[1] == 'stories' && document.getElementsByTagName('a')[0].href == document.getElementsByTagName('a')[1].href){ // accidental story logging seems too easy so here just in case
logData('stories', logStoriesStoryMode())
}
}
});
});
observer.observe(document.getElementById('react-root').children[0].children[0].children[0], {attributes: true, childList: true, characterData: true});
logData('stories', logStoriesStoryMode())
}
function activityMode(){
try{
logActivity(document.getElementsByTagName('main')[0].children[0].children[0].children[0].children[0])
}
catch(e){
setTimeout(activityMode, 1000) // TODO: mutationobserver for this.
}
finally{}
}
function chooseMode(){
if (window.location.href == 'https://www.instagram.com/')
homeMode()
else if (window.location.pathname.split('/')[1] == 'stories')
storyMode()
else if (document.documentElement.classList.contains('touch') && window.location.pathname == '/accounts/activity/') // Desktop activity is in everythingMode
activityMode();
}
/* Detect major page change */ {
var observer = new MutationObserver(function (mutations) {
chooseMode()
});
observer.observe(document.getElementById('react-root'), {childList: true});
}
chooseMode()
}, false);
}
/**
* indexedDB to a download file
* @param {string} filename - name of file w/o extension
* @param {string[]} storeNames - storenames to include in export
* @param {string} dateTimeFormat - only checks for ISO 8601
* @param {string} fileext - accepts json and csv
* @param {number} fileSizeLimit - number of bytes the file can be
* @param {HTMLobject} aElem - HTML object to write the download link to
*/
/* indexedDB to JSON download */ function exportData(filename, storeNames, dateTimeFormat, fileext, fileSizeLimit, aElem) {
var request = indexedDB.open('InstagramLog', 1);
aElem.innerText = 'Creating Download Link...' // This line would be more useful if I did error catching and other statuses
request.onsuccess = function (event) {
var db = event.target.r###lt;
var data = {};
var tx = db.transaction(db.objectStoreNames, 'readonly');
storeNames.forEach(function(storeName) { // use forEach or forleti=0 because openCursor().onsuccess is asynchronous
if (db.objectStoreNames.contains(storeName)){
var store = tx.objectStore(storeName);
if(fileext=='json'){
data[storeName] = [];
let len = JSON.stringify(data).length;
store.openCursor().onsuccess = function (event) {
var cursor = event.target.r###lt;
if (cursor) {
if (dateTimeFormat=='iso')
cursor.value.timestamp=new Date(cursor.value.timestamp * 1000).toISOString()
len+=JSON.stringify(cursor.value).length+1 // length is off by one since a single nonexistent comma was added
if (len - 1 < fileSizeLimit){ // which is why here I subtract 1
data[storeName].push(cursor.value);
cursor.continue();
}
// TODO: range/constraint on cursor
}
};
}
else if (fileext=='csv'){
data[storeName] = '';
let len = 0
Object.keys(data).forEach(function(key) {
len+=(key+'\n').length
len+=(data[key]+'\n').length
})
store.openCursor().onsuccess = function (event) {
var cursor = event.target.r###lt;
if (cursor && len + data[storeName].length < fileSizeLimit) { // size might be a bit bigger since it appends then checks instead of json code which checks then appends
if (dateTimeFormat=='iso')
cursor.value.timestamp=new Date(cursor.value.timestamp * 1000).toISOString()
if(!data[storeName]){
let line = '';
for (let i = 0;i < Object.keys(cursor.value).length; i++){
line+=Object.keys(cursor.value)[i]+','
}
data[storeName]+=(line.slice(0,-1)+'\n')
}
// TODO: range/constraint on cursor
var line = ''
Object.keys(cursor.value).forEach(function(key) {
line+=cursor.value[key]+','
});
data[storeName]+=(line.slice(0,-1)+'\n')
cursor.continue();
}
};
}
}
})
tx.onerror = function () {
console.log('tx.onerror exportData')
db.close();
}
tx.oncomplete = function () {
// Export data
var blob;
if (fileext == 'json'){
blob = new Blob([JSON.stringify(data)], {type: 'octet/stream'});
}
else if (fileext == 'csv'){
var fileParts = [];
Object.keys(data).forEach(function(key) {
fileParts.push(key+'\n')
fileParts.push(data[key]+'\n')
})
blob = new Blob(fileParts, {type: 'octet/stream'});
}
var url = window.URL.createObjectURL(blob);
// Create download link
aElem.href = url;
aElem.innerText = aElem.download = filename + '.' + fileext;
db.close();
};
};
}
/* creating GUI for exporting data */ {
var fab = document.createElement('div'); // this isn't really a floating action button is it?
fab.style = 'z-index:9;position:fixed;top:0;left:25%;height:44px;width:44px;background-color:black;color:white;text-align:center;line-height:normal;font-size:30px;cursor:pointer'
fab.innerHTML = '&darr;'
document.body.insertBefore(fab, document.body.firstChild);
fab.onclick = function(){
/* show/hide overlay thing */
if (!document.getElementById('overlayStuff')){
var overlayStuff = document.createElement('div')
overlayStuff.id="overlayStuff"
overlayStuff.style="z-index:9;position:fixed;background-color:white"
overlayStuff.innerHTML='<table style="width: 100%;"><tbody><tr><td><strong>File Name:</strong></td><td><input id="inputFilename" value="data" type="text"></td></tr><tr><td><strong>Stuff to Save:</strong></td><td><form id="formStuffSave"><input type="checkbox" value="posts" checked>Posts<br><input type="checkbox" value="stories" checked>Stories<br><input type="checkbox" value="activity" checked>Activity<br> </form></td></tr><tr><td><strong>Date/Time Format:</strong></td><td><select id="selectDateTimeFormat"><option value="unix">Unix Time</option><option value="iso">ISO 8601</option></select></td></tr><tr><td><strong>Save as type:</strong></td><td><select id="selectFileType"><option value="csv">CSV - Comma Seperated Values</option><option value="json">JSON - JavaScript Object Notation</option></select></td></tr><tr><td><strong>File Size Limit (MB):</strong></td><td><input id="inputFileSizeLimit" value="50" min="1" type="number"></td></tr></tbody></table><button id="buttonGenerateExport" type="button">Generate File to Download</button> <a id="download"></a>'
//        document.body.appendChild(overlayStuff); // this is same but bottom
document.body.insertBefore(overlayStuff, document.body.firstChild);
document.getElementById('buttonGenerateExport').onclick = function(){
var storeNames = [];
for (var i=0;i<document.getElementById('formStuffSave').elements.length;i++){
if (document.getElementById('formStuffSave').elements[i].checked){
storeNames.push(document.getElementById('formStuffSave').elements[i].value)
}
}
exportData(document.getElementById('inputFilename').value,
storeNames,
document.getElementById('selectDateTimeFormat').value,
document.getElementById('selectFileType').value,
(1e6 * document.getElementById('inputFileSizeLimit').value), // javascript didn't force me to turn it into a number first
document.getElementById('download'))
}
fab.innerHTML = '&uarr;'
}
else {
document.getElementById('overlayStuff').remove()
fab.innerHTML = '&darr;'
}
}
}
// FYI: page does not reload when you go to different places.
// another page TODO: log https://www.instagram.com/p/...
// another page TODO: log https://www.instagram.com/accounts/activity/ (works only for mobile right now)
// another page TODO: log https://www.instagram.com/[username]