July 29, 2019
I built what I like to call a dessert feature on Friday — Instagram embedding.
This is strictly not mission critical; I think I have had a grand total of one person request this feature, but it’s been kicking around my backlog for ages (Buttondown is now at the point where backlog staleness can be measured in years rather than months, which is fun!)
But this is a good feature to build and deliver in two hours:
It’s well-scoped, using primitives that I know my way around.
Buttondown’s markdown rendering platform is one of the more clever pieces of over engineering that I’ve committed myself to, and it means that all I need to do to write up a new embed is this (and this is actual production code):
@generate_block_processor_extension class InstagramExtension(EmbeddedLinkBlockProcessor): prefix = "https://www.instagram.com" def render(self, url): photo = InstagramDataFetcher(url).execute() return render_to_string("instagram.html", {"instagram": photo})
Literally that’s it; the block processor
takes any paragraph that just contains an Instagram link, fetches the data for it, and spits out a template. This means I don’t have to do anything gross with regular expressions or syntax trees or fallback logic; Past Me has already taken care of that.
Fetching the data, though, was a little trickier. Instagram’s API is notoriously volatile and closed-off. I tried a couple ways:
- Applying for the Facebook Developer’s License, which meant I’d be able to hit Instagram’s ‘graph API’. This seemed like a promising front until I realized that I would have to build out a Facebook OAuth flow just for embedding, which eliminated in from contention. (Half for ease-of-use, half for “I don’t want to maintain an authentication flow just for this tiny little feature.)
- Leveraging Instagram’s oembed metadata, which is an easy way of getting some high-level information about a post: who the author is, what the various IDs and captions are, etc. This was a really solid solution, but didn’t quite have everything I wanted — notably like/comment metadata, and author image URL.
- Using the undocumented
__a=1
parameter, which of course is probably the sketchiest solution of all three but by far the easiest. It has all the data I need to fetch — and more.
Once I have JSON data, piping it into a template and writing some basic harness tests is easy:
@dataclass class InstagramDataFetcher: instagram_url: str def execute(self) -> Photo: cached_photo = INSTAGRAM_CACHE.get(self.instagram_url) if cached_photo: return cached_photo response = requests.get(self.instagram_url + "?__a=1").json() media = response["graphql"]["shortcode_media"] photo = Photo( media["edge_media_to_caption"]["edges"][0]["node"]["text"], media["owner"]["username"], f"https://instagram.com/{response['graphql']['shortcode_media']['owner']}", media["owner"]["profile_pic_url"], media["display_url"], self.instagram_url, datetime.fromtimestamp(media["taken_at_timestamp"]), media["edge_media_preview_like"]["count"], media["edge_media_to_parent_comment"]["count"] if "edge_media_to_parent_comment" in media else 0, ) INSTAGRAM_CACHE.set(self.instagram_url, photo) return photo
(I cleaned up the JSON parsing here, but not as much as you’d expect; again, the ‘block processor’ construct gracefully handles errors.)
Because, after all, it was a Friday, I went the “smoke test” route (meaning I am confident in the data being piped in correctly, and I just want to make sure nothing explodes):
class InstagramDataFetcherTestCase(TestCase): @backed_by_network_fixture def test_basic(self): cases = [ "https://www.instagram.com/p/B0W8bYBJN0D/", "https://www.instagram.com/p/B0XaesrBhJl/", "https://www.instagram.com/p/B0WNCUdCtMp/", "https://www.instagram.com/p/B0YVdvYAlMg/", "https://www.instagram.com/p/B0XP7QSnVAm/", "https://www.instagram.com/p/B0Z0MXtnw0H/", "https://www.instagram.com/p/B0YqyoopB9f/", ] for case in cases: InstagramDataFetcher(case).execute()
And voilá. A tiny feature with a tiny test harness. This test harness will grow over time; my Twitter embedding test harness started out the same size, and has grown more voluminous — but in my experience the most interesting edge cases end up revealing themselves in time.
I’m currently auditing a couple different customer service CRM tool things. I have officially reached the point where “answer emails and hope I don’t forget anyone” is an unsustainable strategy for incoming requests and bug reports, which is a nice problem to have but also terrible!
My dream is something along these lines:
- I get an email from someone. (I refuse to have an Intercom-esque help widget in Buttondown. It just feels abstractly gross to me.)
- That email gets thrown into, like, Zendesk or whatever.
- I respond in Zendesk or over email, everything’s kept in sync.
- If I need a code change to solve the issue I can link or create a GitHub Issue right from the interface.
- When I close the issue, I get prompted to resolve the conversation in Zendesk (or whatever).
I am confident this toolchain and workflow exists — this is how pretty much everyone does it, right? — but I still haven’t figured out the right combination of things to search for.