Action

Save Instagram Posts

Posted by Garbonsai, Last update about 2 months ago

UPDATES

about 2 months ago

  • added option to change draft title prefix
  • added option to remove tags from newly created draft(s)
  • added option to delete source draft on success
  • made error handling more robust
  • switched to built in error, info, success, and warning messages
  • cleaned up and refactored code

Searches draft for any Instagram post, reel, or tv URLs and saves the contents of each as a new draft. Does not work for Private Accounts. Edit the script to modify the templates and settings.

Steps

  • script

    // ******************** changelog ******************** //
    
    // v2.0:
    //	- added option to change draft title prefix
    //	- added option to remove tags from newly created draft(s)
    //	- added option to delete source draft on success
    //	- made error handling more robust
    //	- switched to built in error, info, success, and warning messages
    //	- cleaned up and refactored code
    // v1.9.5: added support to archive newly created draft
    // v1.9.4: jump to top of draft if loaded
    // v1.9.3: code cleanup
    // v1.9.2: added support to flag newly created draft
    // v1.9.1: added support for reels and tv
    // v1.9: tweaked default template
    // v1.8: updated to allow for running against multiple drafts at once
    // v1.7: added ability to reuse title of current draft for new draft(s)
    // v1.6: new default template to better accommodate searching Drafts
    // v1.5: gracefully handle non-fatal errors
    // v1.4: added ability to optionally both tag and load newly created drafts
    // v1.3: fix for changes to how paged threaded comments are queried
    // v1.2: code cleanup and refactoring
    // v1.1: now gets all comments, not just most recent
    // v1.0: initial release
    
    // ******************** templates ******************** //
    
    // image template
    var imageTpl = `![]([[postImage]])`;
    
    // comment template
    var commentTpl = `[[commentText]] —[[[commentUser]]](https://www.instagram.com/[[commentUser]]), [[commentDate]]`;
    
    // threaded comment template
    var threadTpl = `[[commentText]] —[[[commentUser]]](https://www.instagram.com/[[commentUser]]), [[commentDate]]`;
    
    // post template
    var postTpl = `[[draftTitle]]
    
    [[postCaption]]
    
    [[postImages]]
    
    [[[postUser]]](https://www.instagram.com/[[postUser]]), [[[postDate]]]([[postUrl]])
    
    ## Comments
    
    [[postComments]]`;
    
    // ******************** settings ******************** //
    
    // prefix for the draft title
    var titlePrefix = '# ';
    
    // prefix for linebreaks found in the caption
    var captionPrefix = '';
    
    // string used to join multiple images together
    var imageGlue = '';
    
    // strftime format string for post and comment dates
    var dateFmt = '%a, %d %b %Y %T %z';
    
    // prefix for linebreaks found in comments
    var commentPrefix = '> ';
    
    // prefix for linebreaks found in threaded comments
    var threadPrefix = '> > ';
    
    // string used to join multiple comments together
    var commentGlue = '\n\n';
    
    // escape hashes found at the beginning of a line in the caption and comments
    var escHashes = false;
    
    // if applicable, reuse the title of the source draft for the new draft(s)
    var reuseTitle = true;
    
    // copy any tags from the source draft to the new draft(s)
    var copyTags = true;
    
    // tags to add to the new draft(s)
    var addTags = [ '@instagram' ];
    
    // tags to remove from the new draft(s)
    var removeTags = [ 'instagram_old' ];
    
    // if no errors or warnings occur, delete the source draft
    var delSource = true;
    
    // archive the newly created draft(s)
    var archiveDraft = true;
    
    // flag the newly created draft(s)
    var flagDraft = true;
    
    // load the new draft(s) in the editor
    var loadDraft = true;
    
    // ******************** functions ******************** //
    
    // log something and alert the user
    function alertUser( log, message, type = 'info' ) {
    	console.log( log );
    	switch ( type ) {
    		case 'error':
    			app.displayErrorMessage( message );
    			delSource = false;
    			break;
    		case 'success':
    			app.displaySuccessMessage( message );
    			break;
    		case 'warning':
    			app.displayWarningMessage( message );
    			delSource = false;
    			break;
    		default:
    			app.displayInfoMessage( message );
    	}
    }
    
    // helper function for sorting comments by date
    function compareDates( a, b ) {
    	if ( a.node.created_at < b.node.created_at ) {
    		return -1;
    	} else if ( a.node.created_at > b.node.created_at ) {
    		return 1;
    	} else {
    		return 0;
    	}
    }
    
    // find any instagram post, reel, or tv URLs
    function findPostUrls( haystack ) {
    	let needle = new RegExp(
    		/(?:https?:\/\/)?(?:w{3}\.)?instagram\.com\/(p|reel|tv)\/[a-zA-Z0-9_\-]+/,
    		'gi'
    	);
    	let matches = [];
    	while ( ( match = needle.exec( haystack ) ) !== null ) matches.push( match[ 0 ] );
    	return matches;
    }
    
    // format a comment block
    function formatComment( comment, prefix, template ) {
    	let text = comment.node.text;
    	if ( escHashes ) text.replace( /^#/gm, '\\#' );
    	text = text.replace( /^/gm, prefix );
    	let date = strftime( comment.node.created_at * 1000, dateFmt );
    
    	// set the template tags and return the processed template
    	draft.setTemplateTag( 'commentText', text );
    	draft.setTemplateTag( 'commentDate', date );
    	draft.setTemplateTag( 'commentUser', comment.node.owner.username );
    	return draft.processTemplate( template );
    }
    
    // get all comments, sorted by date
    function getComments( recentComments, vars, queryHash, type ) {
    	let comments = recentComments.edges;
    	let hasNextPage = recentComments.page_info.has_next_page;
    	vars.after = recentComments.page_info.end_cursor;
    	vars.first = 50;
    
    	// get paged comments
    	while ( hasNextPage ) {
    		let encVars = encodeURIComponent( JSON.stringify( vars ) );
    		let requestUrl = `https://www.instagram.com/graphql/query/?query_hash=${queryHash}&variables=${encVars}`;
    		let request = getUrl( requestUrl );
    		if ( request.success ) {
    
    			// handle errors that can occur when retrieving large numbers of paged comments
    			let response = verifyJSON( request.responseText );
    			if ( !response ) {
    				alertUser(
    					`${requestUrl} didn't return valid JSON`,
    					'Unable to retrieve older comments.',
    					'warning'
    				);
    				hasNextPage = false;
    			} else {
    				let olderComments = type === 'thread' ?
    					response.data.comment.edge_threaded_comments :
    					response.data.shortcode_media.edge_media_to_parent_comment;
    				comments.push.apply( comments, olderComments.edges )
    				hasNextPage = olderComments.page_info.has_next_page;
    				vars.after = olderComments.page_info.end_cursor;
    			}
    		} else {
    			alertUser(
    				`${request.statusCode} ${request.error} for ${requestUrl}`,
    				'Unable to retrieve older comments.',
    				'warning'
    			);
    			hasNextPage = false;
    		}
    	}
    	return comments.sort( compareDates );
    }
    
    // get a URL
    function getUrl( url ) {
    	let http = HTTP.create();
    	return http.request({
    		'method': 'GET',
    		'url': url
    	});
    }
    
    // verify a string is valid JSON
    function verifyJSON( string ) {
    	try {
    		let result = JSON.parse( string );
    		if ( result && typeof result === 'object' ) {
    			return result;
    		}
    	} catch( e ) { }
    	return false;
    }
    
    // ******************** main ******************** //
    
    // check if the draft's title can and should be reused
    reuseTitle = draft.title.substr( 0, titlePrefix.length ) === titlePrefix ?
    	reuseTitle :
    	false;
    
    // find any instagram post, reel, or tv URLs in the draft
    let postUrls = findPostUrls( draft.content );
    if ( postUrls.length === 0 ) {
    	alertUser( '', 'No post, reel, or tv URLs were found.', 'error' );
    } else {
    
    	// get the JSON data for each URL that was found
    	postUrls.forEach( function( postUrl ) {
    		let request = getUrl( `${postUrl}/?__a=1` );
    		if ( request.success ) {
    			let response = verifyJSON( request.responseText );
    			if ( !response ) {
    				alertUser(
    					`${requestUrl} didn't return valid JSON`,
    					'Unable to retrieve data for post, reel, or tv URL.',
    					'error'
    				);
    			} else {
    				let postData = response.graphql.shortcode_media;
    
    				// parse and format the images
    				let postImages = [];
    				if ( postData.hasOwnProperty( 'edge_sidecar_to_children' ) ) {
    					postData.edge_sidecar_to_children.edges.forEach( function( postImage ) {
    						draft.setTemplateTag( 'postImage', postImage.node.display_url );
    						postImages.push( draft.processTemplate( imageTpl ) );
    					});
    				} else {
    					draft.setTemplateTag( 'postImage', postData.display_url );
    					postImages.push( draft.processTemplate( imageTpl ) );
    				}
    
    				// parse and format the caption
    				let postCaption = postData.edge_media_to_caption.edges[0].node.text;
    				if ( escHashes ) postCaption = postCaption.replace( /^#/gm, '\\#' );
    				postCaption  = postCaption.replace( /^/gm, captionPrefix );
    
    				// parse and format the comments
    				let postComments = [];
    				let comments = getComments(
    					postData.edge_media_to_parent_comment,
    					{ shortcode: postData.shortcode },
    					'97b41c52301f77ce508f55e66d17620e',
    					'comment'
    				);
    				comments.forEach( function( comment ) {
    					postComments.push( formatComment( comment, commentPrefix, commentTpl ) );
    					let threads = getComments(
    						comment.node.edge_threaded_comments,
    						{ comment_id: comment.node.id },
    						'51fdd02b67508306ad4484ff574a0b62',
    						'thread'
    					);
    					threads.forEach( function( thread ) {
    						postComments.push( formatComment( thread, threadPrefix, threadTpl ) );
    					});
    				});
    
    				// set the necessary template tags
    				draft.setTemplateTag( 'postUrl', postUrl );
    				draft.setTemplateTag( 'postImages', postImages.join( imageGlue ) );
    				draft.setTemplateTag( 'postCaption', postCaption );
    				draft.setTemplateTag( 'postComments', postComments.join( commentGlue ) );
    				draft.setTemplateTag( 'postUser', postData.owner.username );
    				draft.setTemplateTag( 'postDate', strftime( postData.taken_at_timestamp * 1000, dateFmt ) );
    				reuseTitle ?
    					draft.setTemplateTag( 'draftTitle', draft.title ) :
    					draft.setTemplateTag( 'draftTitle', titlePrefix + postData.shortcode );
    
    				// create the new draft
    				let d = Draft.create();
    				d.content = draft.processTemplate( postTpl );
    				if ( copyTags ) draft.tags.forEach( tag => d.addTag( tag ) );
    				addTags.forEach( tag => d.addTag( tag ) );
    				removeTags.forEach( tag => d.removeTag( tag ) );
    				d.isFlagged = flagDraft;
    				d.isArchived = archiveDraft;
    				d.update();
    				if ( loadDraft ) {
    					editor.load( d );
    					editor.setSelectedRange( 0, 0 );
    				}
    
    				// delete the source draft
    				draft.isTrashed = delSource;
    				draft.update();
    
    			}
    		} else {
    			alertUser(
    				`${request.statusCode} ${request.error} for ${requestUrl}`,
    				'Unable to retrieve data for post, reel, or tv URL.',
    				'error'
    			);
    		}
    	});
    }
    

Options

  • After Success Nothing
    Notification Error
    Log Level Error
Items available in the Drafts Directory are uploaded by community members. Use appropriate caution reviewing downloaded items before use.