Action

Save Instagram Posts

Posted by garbonsai, Last update 9 months ago

UPDATES

9 months ago

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

show all updates...

9 months ago

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

9 months ago

Added support for reels.

9 months ago

Change keyboard shortcut to avoid conflict.

9 months 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 2 years 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.