Action

Save Instagram Posts

Posted by garbonsai, Last update 19 days ago

UPDATES

19 days ago

Bugfix for posts that have been removed (soft 404s).

show all updates...

19 days ago

Bugfix for posts that have been removed (soft 404s).

19 days ago

Added support for reels.

20 days ago

Change keyboard shortcut to avoid conflict.

20 days ago

This is a complete rewrite due to Meta turning off the JSON endpoint for Instagram posts. Unfortunately, comments can no longer be retrieved and I’m still working on TV and reels support.

over 1 year 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 or reel URLs and saves the contents of each as a new draft. Does not work for Private Accounts. Edit the template tags to modify the templates and settings.

Steps

  • defineTemplateTag

    name
    imageTemplate
    template
    ![]([[postImage]])
  • defineTemplateTag

    name
    videoTemplate
    template
  • defineTemplateTag

    name
    postTemplate
    template
    [[draftTitle]]
    
    [[postCaption]]
    
    [[postMedia]]
    
    [[[postUser]]](https://www.instagram.com/[[postUser]]), [[[postDate]]]([[postUrl]])
  • defineTemplateTag

    name
    titlePrefix
    template
    # 
  • defineTemplateTag

    name
    captionPrefix
    template
  • defineTemplateTag

    name
    mediaGlue
    template
  • defineTemplateTag

    name
    dateFormat
    template
    %a, %d %b %Y %T %z
  • defineTemplateTag

    name
    escapeHashes
    template
    false
  • defineTemplateTag

    name
    reuseTitle
    template
    false
  • defineTemplateTag

    name
    copyTags
    template
    true
  • defineTemplateTag

    name
    tagsToAdd
    template
    @instagram
  • defineTemplateTag

    name
    tagsToRemove
    template
    instagram_old
  • defineTemplateTag

    name
    archiveNewDrafts
    template
    true
  • defineTemplateTag

    name
    flagNewDrafts
    template
    true
  • defineTemplateTag

    name
    loadNewDrafts
    template
    true
  • script

    // ******************** changelog ******************** //
    
    // v3.0.3: bug fix for posts that have been removed
    // v3.0.2: added support for reels
    // v3.0.1: changed keyboard shortcut
    // v3.0:
    // - complete rewrite because meta turned off JSON endpoint - thanks meta :(
    // - now uses the JSON-LD embedded in each post's HTML on Instagram
    // - now uses template tags for settings and templates
    // - now honors action's 'After Success' setting
    // - remove ability to retrieve comments - thanks meta :(
    // v2.0.2: fix for 'Script Error: ReferenceError: Can't find variable: requestUrl'
    // v2.0.1: added option to delete source draft on warning and error
    // 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 ******************** //
    
    // default video template
    const defVideoTemplate =
    	'<video controls height="[[postVideoHeight]]" width="[[postVideoWidth]]" ' +
    	'style="height:auto;max-width:[[postVideoWidth]]px;width:100%"><source ' +
    	'src="[[postVideoUrl]]" type="video/mp4"></video>';
    
    // default post template
    const defPostTemplate = `[[draftTitle]]
    
    [[postCaption]]
    
    [[postMedia]]
    
    [[[postUser]]](https://www.instagram.com/[[postUser]]), [[[postDate]]]([[postUrl]])`;
    
    // ******************** functions ******************** //
    
    // alert the user and handle breaking errors
    function alertUser(type, message, log) {
    	switch (type) {
    		case 'error':
    			app.displayErrorMessage(message);
    			throw log;
    			break;
    		case 'success':
    			app.displaySuccessMessage(message);
    			break;
    		default:
    			app.displayInfoMessage(message);
    	}
    }
    
    // convert 'true' and 'false' to boolean or return a default
    function strToBool(string, otherwise = false) {
    	return string === 'true' || string === 'false'
    		? JSON.parse(string)
    		: otherwise;
    }
    
    // check if a template tag exists and if not, return a default
    function ttExists(tag, otherwise) {
    	return tag !== '' ? tag : otherwise;
    }
    
    // verify a string is valid JSON
    function verifyJson(string) {
    	let result = JSON.parse(string);
    	if (!result || typeof result !== 'object') {
    		alertUser(
    			'error',
    			'JSON-LD is not valid.',
    			`Invalid JSON-LD: ${string}`
    		);
    	}
    	return result;
    }
    
    // ******************** main ******************** //
    
    // set various options using template tags or defaults
    const imageTemplate = ttExists(
    	draft.getTemplateTag('imageTemplate'),
    	'![]([[postImage]])'
    );
    const videoTemplate = ttExists(
    	draft.getTemplateTag('videoTemplate'),
    	defVideoTemplate
    );
    const postTemplate = ttExists(
    	draft.getTemplateTag('postTemplate'),
    	defPostTemplate
    );
    const titlePrefix = ttExists(draft.getTemplateTag('titlePrefix'), '# ');
    const captionPrefix = draft.getTemplateTag('captionPrefix');
    const mediaGlue = draft.getTemplateTag('mediaGlue');
    const dateFormat = ttExists(
    	draft.getTemplateTag('dateFormat'),
    	'%a, %d %b %Y %T %z'
    );
    const escapeHashes = strToBool(draft.getTemplateTag('escapeHashes'));
    const reuseTitle = strToBool(draft.getTemplateTag('reuseTitle'));
    const copyTags = strToBool(draft.getTemplateTag('copyTags', true));
    const tagsToAdd = draft.getTemplateTag('tagsToAdd').split(',');
    const tagsToRemove = draft.getTemplateTag('tagsToRemove').split(',');
    const archiveNewDrafts = strToBool(draft.getTemplateTag('archiveNewDrafts'));
    const flagNewDrafts = strToBool(draft.getTemplateTag('flagNewDrafts'));
    const loadNewDrafts = strToBool(draft.getTemplateTag('loadNewDrafts', true));
    
    // find any Instagram post URLs in the draft
    try {
    	let postUrls = [];
    	let postUrlRegExp = new RegExp(
    		/(?:https?:\/\/)?(?:w{3}\.)?instagram\.com\/(p|reel)\/[a-zA-Z0-9_\-]+/,
    		'gi'
    	);
    	let postUrlMatch;
    	while ((postUrlMatch = postUrlRegExp.exec(draft.content)) !== null) {
    		postUrls.push([postUrlMatch[0], postUrlMatch[1]]);
    	}
    	if (postUrls.length === 0) {
    		alertUser('error', 'No post URLs found.', 'No post URLs found.');
    	}
    
    	// retrieve each post URL that was found
    	postUrls.forEach(function (postUrl) {
    		let http = HTTP.create();
    		let httpRequest = http.request({
    			method: 'GET',
    			url: postUrl[0],
    		});
    		if (!httpRequest.success) {
    			alertUser(
    				'error',
    				'Unable to retrieve post.',
    				`Unable to retrieve ${postUrl[0]}.`
    			);
    		}
    
    		// extract the JSON-LD post data
    		let jsonRegExp = new RegExp(
    			/<script type="application\/ld\+json" nonce=".*?">(.*?)<\/script>/,
    			'i'
    		);
    		let jsonData = jsonRegExp.exec(httpRequest.responseText);
    		if (jsonData === null) {
    			alertUser(
    				'error',
    				'No JSON-LD found.',
    				`No JSON-LD found at ${postUrl[0]}.`
    			);
    		}
    		let postData = verifyJson(jsonData[1]);
    
    		// format the post media
    		let postMedia = [];
    		if (postUrl[1] === 'p') {
    			postData.image.forEach(function (postImage) {
    				draft.setTemplateTag('postImage', postImage.url);
    				postMedia.push(draft.processTemplate(imageTemplate));
    			});
    		} else if (postUrl[1] === 'reel') {
    			postData.video.forEach(function (postVideo) {
    				draft.setTemplateTag('postVideoUrl', postVideo.contentUrl);
    				draft.setTemplateTag('postVideoHeight', postVideo.height);
    				draft.setTemplateTag('postVideoWidth', postVideo.width);
    				postMedia.push(draft.processTemplate(videoTemplate));
    			});
    		}
    
    		// format the post caption
    		let postCaption = postData.articleBody;
    		if (escapeHashes) postCaption = postCaption.replace(/^#/gm, '\\#');
    		postCaption = postCaption.replace(/^/gm, captionPrefix);
    
    		// format the post date
    		let postDate = strftime(new Date(postData.dateCreated), dateFormat);
    
    		// set the necessary template tags
    		draft.setTemplateTag('postUrl', postUrl[0]);
    		draft.setTemplateTag('postMedia', postMedia.join(mediaGlue));
    		draft.setTemplateTag('postCaption', postCaption);
    		draft.setTemplateTag('postUser', postData.author.identifier.value);
    		draft.setTemplateTag('postDate', postDate);
    		reuseTitle
    			? draft.setTemplateTag('draftTitle', draft.title)
    			: draft.setTemplateTag(
    					'draftTitle',
    					titlePrefix + postData.identifier.value
    			  );
    
    		// create the new draft
    		let d = Draft.create();
    		d.content = draft.processTemplate(postTemplate);
    		if (copyTags) draft.tags.forEach((tag) => d.addTag(tag));
    		tagsToAdd.forEach((tag) => d.addTag(tag));
    		tagsToRemove.forEach((tag) => d.removeTag(tag));
    		d.isFlagged = flagNewDrafts;
    		d.isArchived = archiveNewDrafts;
    		d.update();
    
    		// load the new draft
    		if (loadNewDrafts) {
    			editor.load(d);
    			editor.setSelectedRange(0, 0);
    		}
    	});
    
    	// display a success notification
    	alertUser('success', 'Post successfully saved.', '');
    
    	// log thrown errors
    } catch (e) {
    	console.log(e);
    }
    

Options

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