<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Abdeldjalil Hebal</title>
    <description>The latest articles on DEV Community by Abdeldjalil Hebal (@djalilhebal).</description>
    <link>https://dev.to/djalilhebal</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F305069%2Fee53d8b5-8f14-4dc3-9f08-c21cb69c7c67.jpg</url>
      <title>DEV Community: Abdeldjalil Hebal</title>
      <link>https://dev.to/djalilhebal</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/djalilhebal"/>
    <language>en</language>
    <item>
      <title>Debugging Data Pipelines: From Memory to File with WebDAV</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Sat, 03 May 2025 15:18:34 +0000</pubDate>
      <link>https://dev.to/djalilhebal/debugging-data-pipelines-from-memory-to-file-with-webdav-n77</link>
      <guid>https://dev.to/djalilhebal/debugging-data-pipelines-from-memory-to-file-with-webdav-n77</guid>
      <description>&lt;p&gt;Debugging complex data pipelines often involves keeping track of short-lived data--things like Excel buffers, normalized inputs, chart images, and transformed data frames. \&lt;br&gt;
You could log some JSON and start guessing: "Was it the parser in the transform step with the off-by-one error?" But what if you could... just open it in a UI?&lt;/p&gt;

&lt;p&gt;This post outlines a setup that turns transient data into inspectable artifacts, with almost zero friction, improving (my) the developer experience dramatically.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem: Poor Debugging Experience
&lt;/h2&gt;

&lt;p&gt;Suppose you are working on a complex data-heavy system, like a business intelligence platform called Pie.&lt;/p&gt;

&lt;p&gt;You need to debug its internal data processing pipelines--how data changes from stage to stage, from start to finish.&lt;/p&gt;

&lt;p&gt;The QA team might benefit from checking some specific pieces of information that are otherwise hard to validate end-to-end. Your PM might ask you to send them the actual Excel file and formulas being used, so you have to generate a file*, copy it, and email it. This might happen more than once as business requirements and implementation change.&lt;/p&gt;

&lt;p&gt;This becomes a recurring pain point with poor developer experience.&lt;/p&gt;

&lt;p&gt;(* There might be no file or even file-like thing. You may be working with data frames (Pandas or &lt;a href="https://github.com/pola-rs/polars" rel="noopener noreferrer"&gt;Polars&lt;/a&gt;), event streams, and whatnot.)&lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: A Poor Man's Google Drive for Debug Data
&lt;/h2&gt;
&lt;h3&gt;
  
  
  The Concept
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;ideal approach&lt;/strong&gt; would be to automatically export the in-memory data as a file and upload it to Google Drive, making it easy to open on Google Sheets, for example.&lt;/p&gt;

&lt;p&gt;Up until recently, my &lt;strong&gt;crude approach&lt;/strong&gt; was to write data as &lt;code&gt;.tsv&lt;/code&gt; in a local folder when running on my machine. I would open it using LibreOffice Calc or import it into Google Sheets since they support &lt;code&gt;.tsv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After going down one too many rabbit holes--and nearly building a content repository just to debug Excel files--I stumbled onto a simpler, more &lt;strong&gt;streamlined approach&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Treat in-memory data as a file, upload it to a WebDAV server, and inspect with Filestash.&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  The Architecture
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi51g9v2s3ene4kvu7fl1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi51g9v2s3ene4kvu7fl1.png" alt="Backend, WebDAV server, web file manager (Filestash), web office (Collabora Online)" width="800" height="290"&gt;&lt;/a&gt;&lt;br&gt;
Main components and protocols&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This setup combines the following core technologies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;WebDAV Server&lt;/strong&gt;: Stores debug data files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Filestash&lt;/strong&gt;: Provides a web UI to browse files (think: minimalist Google Drive)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collabora Online&lt;/strong&gt;: Embedded in Filestash to preview office files (think: web LibreOffice)&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;
  
  
  Why WebDAV?
&lt;/h3&gt;

&lt;p&gt;WebDAV is an HTTP-based &lt;strong&gt;&lt;em&gt;standard&lt;/em&gt;&lt;/strong&gt; with broad support across OS file managers (e.g., Windows Explorer or &lt;a href="https://support.apple.com/en-gb/guide/mac-help/mchlp1546/mac" rel="noopener noreferrer"&gt;Mac's thing&lt;/a&gt;). It's simple to integrate&lt;sup id="fnref1"&gt;1&lt;/sup&gt;, future-proof enough, and has many server implementations: Jackrabbit, Apache Server, and various standalone options.&lt;/p&gt;

&lt;p&gt;I've always wondered what the hell was the deal with &lt;a href="https://jackrabbit.apache.org/" rel="noopener noreferrer"&gt;Apache Jackrabbit&lt;/a&gt;, WebDAV, CalDAV, etc. Now, they are starting to make sense.&lt;/p&gt;

&lt;p&gt;Apache Jackrabbit is a low-level content repository—think of it as a headless CMS&lt;sup id="fnref2"&gt;2&lt;/sup&gt; that also supports WebDAV. I liked the idea of using it for its advanced features—node graphs, querying, tagging—but I couldn't get it running locally. And honestly, it was overkill. A simple WebDAV server would do, so I tried a few and settled on &lt;code&gt;dufs&lt;/code&gt;. It's good enough for now. I might revisit Jackrabbit later if the extra features become useful.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://experienceleague.adobe.com/en/docs/experience-manager-cloud-service/content/screens-as-cloud-service/developing-screens-cloud/rest-apis-screens-cloud" rel="noopener noreferrer"&gt;AEM Screens – REST API&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Possible alternatives:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;[ ] MinIO (S3-compatible local storage).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I've tested MinIO. See &lt;code&gt;/debug-drive-minio&lt;/code&gt; in the GitHub repo.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;[ ] FTP (FTPS or SFTP).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like, just check &lt;a href="https://github.com/mickael-kerjean/filestash/blob/2c09815c4e5f6bd681a3a5cf88b24774848f2da5/server/plugin/plg_backend_webdav/index.go" rel="noopener noreferrer"&gt;Filestash's WebDAV adapter&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Why Filestash?
&lt;/h3&gt;

&lt;p&gt;Searched "webdav" on GitHub, sorted by stars, and skimmed the top results. Filestash stood out. The missing piece I didn't know I needed—until I did.&lt;/p&gt;

&lt;p&gt;Filestash serves a clean web UI to our storage backend—think: a minimalist, local Google Drive. It uses Collabora Online (think: web LibreOffice) to preview office files.&lt;/p&gt;

&lt;p&gt;Try &lt;a href="https://demo.filestash.app/login?type=webdav&amp;amp;url=https%3A%2F%2Fwebdav.filestash.app&amp;amp;username=&amp;amp;password=" rel="noopener noreferrer"&gt;their demo&lt;/a&gt; (Documents &amp;gt; Office.xlsx).&lt;/p&gt;

&lt;p&gt;Pros:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Clean and user-friendly interface.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The storage server (e.g. WebDAV or S3) needs to be accessible only to the backend server and Filestash server.&lt;br&gt;
Meaning, that only Filestash needs to be exposed to the internet.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Cons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;No descending sort support.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;LibreOffice Calc (and the web version) handles &lt;code&gt;.csv&lt;/code&gt; and &lt;code&gt;.tsv&lt;/code&gt; just fine, but Filestash opens them in a basic text viewer.&lt;br&gt;
As far as I can tell, there's no UI option to change the default viewer or editor per file type.&lt;br&gt;
You'd probably need to dive into plugin code (written in Go) and recompile it to get that working.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Workaround: Just convert the &lt;code&gt;.tsv&lt;/code&gt; to a proper Excel file.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Possible alternatives:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] &lt;a href="https://nextcloud.com/" rel="noopener noreferrer"&gt;Nextcloud&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;It uses either Collabora Online or OnlyOffice.&lt;/li&gt;
&lt;li&gt;Seems to be trying to do too much, but it's popular.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Implementation: Step by Step
&lt;/h2&gt;

&lt;p&gt;To demonstrate the approach, I built a simplified proof-of-concept.&lt;/p&gt;
&lt;h3&gt;
  
  
  Scenario: Analytics Platform
&lt;/h3&gt;

&lt;p&gt;Imagine you are developing an analytics platform called Pie.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Operation:&lt;/strong&gt; Generate Excel files with pie charts from query parameters.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Get labels and their associated values from the request.&lt;/li&gt;
&lt;li&gt;Generate a pie chart image (in-memory).&lt;/li&gt;
&lt;li&gt;Generate an Excel file with that chart (in-memory).&lt;/li&gt;
&lt;li&gt;Send that file as a response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local version (run &lt;code&gt;pie-generator-backend&lt;/code&gt;): &lt;a href="http://127.0.0.1:3000/pie?a=10&amp;amp;b=20&amp;amp;c=70" rel="noopener noreferrer"&gt;http://127.0.0.1:3000/pie?a=10&amp;amp;b=20&amp;amp;c=70&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Live version (deployed on Vercel): &lt;a href="https://debugging-data-pipelines-demo.vercel.app/pie?a=10&amp;amp;b=20&amp;amp;c=70" rel="noopener noreferrer"&gt;https://debugging-data-pipelines-demo.vercel.app/pie?a=10&amp;amp;b=20&amp;amp;c=70&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It would generate an Excel file containing a pie chart:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu705emm7268quwzieuf1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fu705emm7268quwzieuf1.png" width="800" height="471"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Setting Up the WebDAV Server
&lt;/h3&gt;

&lt;p&gt;Using &lt;a href="https://github.com/sigoden/dufs" rel="noopener noreferrer"&gt;&lt;code&gt;dufs&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dufs &lt;span class="nt"&gt;--auth&lt;/span&gt; admin:admin@/:rw &lt;span class="nt"&gt;--allow-all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Writing Code for the Client
&lt;/h3&gt;

&lt;p&gt;Using &lt;a href="https://github.com/perry-mitchell/webdav-client" rel="noopener noreferrer"&gt;&lt;code&gt;webdav&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;createClient&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;webdav&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http://192.168.100.11:5000/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;admin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putFileContents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/omega/deep/test.txt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;some text&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Setting Up Filestash
&lt;/h3&gt;

&lt;p&gt;See &lt;a href="https://www.filestash.app/docs/install-and-upgrade/" rel="noopener noreferrer"&gt;Filestash install guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Basically, just use Docker Compose to set up Filestash and Collabora Online and link them together.&lt;/p&gt;

&lt;h3&gt;
  
  
  Making it Reusable
&lt;/h3&gt;

&lt;p&gt;Instead of wiring the WebDAV logic directly into each task, I abstracted it into a helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveToWebdav&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;WEB_DAV_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putFileContents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything runs in Docker, so setup -- whether local or remote -- is painless.&lt;br&gt;
I added &lt;code&gt;dufs&lt;/code&gt; to Filestash's &lt;code&gt;docker-compose.yml&lt;/code&gt;`&lt;sup id="fnref3"&gt;3&lt;/sup&gt;, so everything starts together with a single command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;/code&gt;`sh&lt;br&gt;
cd debug-drive&lt;/p&gt;

&lt;h1&gt;
  
  
  Sets up dufs WebDAV server, Filestash, and Collabora Online
&lt;/h1&gt;

&lt;p&gt;sudo docker compose up -d&lt;/p&gt;

&lt;p&gt;sudo docker compose down&lt;br&gt;
`&lt;code&gt;&lt;/code&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Results
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Login&lt;/th&gt;
&lt;th&gt;Browsing logs&lt;/th&gt;
&lt;th&gt;Previewing Excel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx2sene74e82uw6rbo0nz.png" width="800" height="869"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdgcqha9dvnp88cyrkh4m.png" width="800" height="869"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fucjnvx8kzvym9b4elghl.png" width="800" height="869"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Big Wins
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Simplifies debugging.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Better transparency and observability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;We can log the saved file's URL using any existing logging infrastructure (e.g. Datadog, Sentry, or OpenObserve).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Challenges and Considerations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;File name conflicts&lt;/strong&gt;:&lt;br&gt;
Easily solvable: Each operation or job should use a unique prefix or folder. This should be a "Correlation ID" if it exists.&lt;br&gt;
Otherwise, a random (preferably sortable) key would do, something like &lt;a href="https://en.wikipedia.org/wiki/Snowflake_ID" rel="noopener noreferrer"&gt;Snowflake ID&lt;/a&gt; or &lt;a href="https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_7_(timestamp_and_random)" rel="noopener noreferrer"&gt;UUID v7&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data retention policy&lt;/strong&gt;:&lt;br&gt;
When and how to delete old files/folders?&lt;br&gt;
Apache Jackrabbit supports querying nodes and lets us specify custom metadata (e.g. "neverExpires" or "expiresOn" or whatever). That simplifies data manipulation.&lt;br&gt;
Still, we could use simple deletion logic (e.g. older than 3 days) via a &lt;code&gt;cron&lt;/code&gt; job.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;How to send data to the server&lt;/strong&gt;:&lt;br&gt;
Options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[x] Install a WebDAV client (I mean, what's a new JS dependency).&lt;/li&gt;
&lt;li&gt;[ ] Use a WebDAV server that has a RESTful API: &lt;a href="https://github.com/drakkan/sftpgo" rel="noopener noreferrer"&gt;SFTPGo&lt;/a&gt;, for example.&lt;/li&gt;
&lt;li&gt;[ ] Use S3/MinIO. It works best if we only need a REST API. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Performance considerations&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each task generates only a couple of files.&lt;/li&gt;
&lt;li&gt;Overhead does not matter since we only use it for debugging locally. We might enable it in dev, but obviously not prod.&lt;/li&gt;
&lt;li&gt;Network overhead does not matter since the app backend and WebDAV servers are on the same "machine".&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;WebDAV + Filestash makes for a surprisingly elegant debugging solution for data-heavy applications.&lt;/p&gt;

&lt;p&gt;As always, there's room for improvement:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We need proper data retention and cleanup mechanisms.&lt;/li&gt;
&lt;li&gt;The Docker setup could use some DevOps care.&lt;/li&gt;
&lt;li&gt;Access control should be tightened (Filestash only needs read access).&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;The experiment repo is available at &lt;a href="https://github.com/djalilhebal/debugging-data-pipelines-demo" rel="noopener noreferrer"&gt;https://github.com/djalilhebal/debugging-data-pipelines-demo&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;END.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;WebDAV is so simple that you can create a client using any HTTP client. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;It's not a CMS itself, but you can build one on top of it. Adobe Experience Manager does this, exposing content via a RESTful API. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;The Docker Compose thing doesn't follow best practices and could be polished. I am not a DevOps engineer and even I can realize that. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>debugging</category>
      <category>webdev</category>
      <category>nodejs</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Debugging Complex SQL Queries: A Structured Logging Approach</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Tue, 04 Mar 2025 17:58:27 +0000</pubDate>
      <link>https://dev.to/djalilhebal/debugging-complex-sql-queries-a-structured-logging-approach-200m</link>
      <guid>https://dev.to/djalilhebal/debugging-complex-sql-queries-a-structured-logging-approach-200m</guid>
      <description>&lt;p&gt;TLDR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;json_build_object&lt;/code&gt; to convert relations (tables or CTE/WITH clauses) to JSON.&lt;/li&gt;
&lt;li&gt;Write a helper JavaScript function that abstracts the approach.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Background and problem
&lt;/h2&gt;

&lt;p&gt;Imagine you are working on something exciting, colorful, and wonderful like a warehouse management system.&lt;/p&gt;

&lt;p&gt;As the system evolves, new features, requirements, and processing logic keep expanding,&lt;br&gt;
SQL queries start becoming more complex, so you resort to using &lt;code&gt;WITH&lt;/code&gt; clauses to keep everything organized and maintainable. It works great.&lt;/p&gt;

&lt;p&gt;However, when it comes to debugging intermediate steps/states/variables,&lt;br&gt;
while in the application layer, you start reaching out to structured logging libraries like &lt;a href="https://github.com/pinojs/pino" rel="noopener noreferrer"&gt;JavaScript's Pino&lt;/a&gt; or &lt;a href="https://go.dev/blog/slog" rel="noopener noreferrer"&gt;Golang's slog&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;

&lt;p&gt;Postgres has advanced JSON support, so why not do something like the following?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;data2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;data2&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In SQL, that would translate to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;json_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'data1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="s1"&gt;'data2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data2&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;data1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, in a way to avoid cross joins and using JSONB (supposedly both are better for performance):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s1"&gt;'data1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;data1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="s1"&gt;'data2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;data2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example
&lt;/h2&gt;

&lt;p&gt;Imagine your project manager assigns the following user story to you:&lt;/p&gt;

&lt;blockquote&gt;
&lt;h2&gt;
  
  
  User Story: Inventory Reorder Alert
&lt;/h2&gt;

&lt;p&gt;As a warehouse manager,&lt;br&gt;
I need a report identifying products requiring immediate reorder based on low total stock quantities across all locations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use the provided sample data (locations: S1, S2, S3; products and quantities).&lt;/li&gt;
&lt;li&gt;Calculate the &lt;strong&gt;total quantity per product&lt;/strong&gt; across all locations.&lt;/li&gt;
&lt;li&gt;Flag products as &lt;strong&gt;Reorder&lt;/strong&gt; if total stock is below 50 units; otherwise, mark as &lt;strong&gt;Sufficient&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Display &lt;strong&gt;only products needing reorder&lt;/strong&gt; in the final output.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Acceptance Criteria:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Output columns: &lt;code&gt;product&lt;/code&gt;, &lt;code&gt;stock_status&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Only include rows where &lt;code&gt;stock_status&lt;/code&gt; is &lt;strong&gt;Reorder&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;You could solve it using a single SQL query.&lt;/p&gt;

&lt;p&gt;Notice that we are using CTEs and are only interested in Step 1 and Step 2.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Define raw data using the VALUES clause.&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;location&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;VALUES&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'S1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cocoa beans'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'S2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Cocoa beans'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'S2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sugar'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'S3'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Vanilla'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;75&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Aggregation: Get total sum per product.&lt;/span&gt;
&lt;span class="n"&gt;step1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;raw_data&lt;/span&gt;
    &lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Transformation: Add a stock status based on total quantity.&lt;/span&gt;
&lt;span class="n"&gt;step2&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
         &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
         &lt;span class="n"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="k"&gt;CASE&lt;/span&gt;
            &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;quantity&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt; &lt;span class="s1"&gt;'Reorder'&lt;/span&gt;
            &lt;span class="k"&gt;ELSE&lt;/span&gt; &lt;span class="s1"&gt;'Sufficient'&lt;/span&gt;
        &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;stock_status&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step1&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="c1"&gt;-- Filter&lt;/span&gt;
&lt;span class="n"&gt;step3&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt;
        &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stock_status&lt;/span&gt;
    &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step2&lt;/span&gt;
    &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;stock_status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Reorder'&lt;/span&gt;
&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="n"&gt;logged_steps&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_build_object&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;'step1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="s1"&gt;'step2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;jsonb_agg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;step2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;json_payload&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;step3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logged_steps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Better DX
&lt;/h2&gt;

&lt;p&gt;We can reuse this logic with the help of query builders like &lt;a href="https://github.com/knex/knex" rel="noopener noreferrer"&gt;Knex&lt;/a&gt; and its &lt;a href="https://knexjs.org/guide/query-builder.html#modify" rel="noopener noreferrer"&gt;&lt;code&gt;modify&lt;/code&gt; method&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;withLoggedSteps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;qb&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;relations&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;qb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;crossJoin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logged_steps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;qb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logged_steps.json_payload&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;relations&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`'&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;', (SELECT jsonb_agg(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) FROM &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;rel&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;qb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;logged_steps&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;knex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`SELECT jsonb_build_object(&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;) AS json_payload`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;knex&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step3.*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;generated_data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;(VALUES ...)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;modify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withLoggedSteps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;step2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Interactive demo
&lt;/h2&gt;

&lt;p&gt;See: &lt;a href="https://debugging-complex-sql-demo.vercel.app/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;PGLite in-memory embedded Postgres database.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Knex and a PGLite "driver"&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://pglite.dev/docs/orm-support#knex-js" rel="noopener noreferrer"&gt;https://pglite.dev/docs/orm-support#knex-js&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/czeidler/knex-pglite" rel="noopener noreferrer"&gt;https://github.com/czeidler/knex-pglite&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Some REPL library (&lt;code&gt;vue-live&lt;/code&gt;)&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Performance Disclaimer
&lt;/h2&gt;

&lt;p&gt;Let's be honest - in my day-to-day development, I'm typically working with small, filtered datasets.&lt;br&gt;
Most of the time, I'm debugging queries for a specific product or a narrow time range.&lt;/p&gt;

&lt;p&gt;So while these JSON logging techniques work wonderfully in my development environment, I haven't extensively tested them on massive datasets that might process millions of rows.&lt;/p&gt;

&lt;p&gt;When you're in development mode, logging overhead is practically unimportant.&lt;br&gt;
We're here to understand why our queries behave strangely and to figure out where your complex query goes wrong.&lt;/p&gt;

&lt;p&gt;I am aware of alternative approaches like using &lt;code&gt;RAISE NOTICE&lt;/code&gt; or inserting intermediate results into temporary tables. I have not tested them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;And there you have it, implementing JSON structured logging in SQL thanks to Postgres features.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://debugging-complex-sql-demo.vercel.app/" rel="noopener noreferrer"&gt;Live demo&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;END.&lt;/p&gt;

</description>
      <category>postgres</category>
    </item>
    <item>
      <title>Using SVG &lt;symbol&gt; as a Camera (Danganronpa Back and Forth)</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Tue, 06 Feb 2024 13:05:00 +0000</pubDate>
      <link>https://dev.to/djalilhebal/using-svg-as-a-camera-danganronpa-back-and-forth-2gbj</link>
      <guid>https://dev.to/djalilhebal/using-svg-as-a-camera-danganronpa-back-and-forth-2gbj</guid>
      <description>&lt;p&gt;I like SVG. I like Danganronpa. I tried to recreate a Danganronpa-like camera panning effect in SVG.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxem24e10hztgoi8cnwr.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frxem24e10hztgoi8cnwr.gif" alt="Demo" width="320" height="180"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TLDR:&lt;/strong&gt;&lt;br&gt;
We can use SVG &lt;code&gt;&amp;lt;symbol&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;use&amp;gt;&lt;/code&gt; elements to create a camera, similar to the ones you may find in video editors, game engines, and the like (After Effects, Unity, and Blender, to name a few).&lt;br&gt;
The goal was to recreate a panning effect as seen in Danganronpa games or fan projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PS: This is not a step-by-step tutorial.&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This is mostly a proof of concept.&lt;/li&gt;
&lt;li&gt;I will provide a high-level overview, mention specific implementation details, and link to further readings.&lt;/li&gt;
&lt;li&gt;I assume you are somewhat familiar with CSS animations and SVG.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The idea
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Danganronpa panning effect
&lt;/h3&gt;

&lt;p&gt;In official Danganronpa games and fan projects: &lt;a href="https://youtu.be/EGU4w5C_WKI?list=PLw3Hoj70YKZBdEBcpDKu8GiXG_WBVysrp&amp;amp;t=1398" rel="noopener noreferrer"&gt;Danganronpa F: Shattered Hope&lt;/a&gt; (made in After Effects), &lt;a href="https://youtu.be/-U6aCDrH3NA?t=8581" rel="noopener noreferrer"&gt;Project: Eden's Garden&lt;/a&gt; (made in Unity), and even &lt;a href="https://www.youtube.com/watch?v=wEQNIgRscCY" rel="noopener noreferrer"&gt;some memes/remixes&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If I recall correctly, in Danganronpa 2, the following sequence happens:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;At the start of the animation, blur all characters except the active speaker.&lt;/li&gt;
&lt;li&gt;Move the camera to the active speaker.&lt;/li&gt;
&lt;li&gt;At the end of the animation, unblur all characters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We could try to recreate that.&lt;/p&gt;

&lt;p&gt;We also could make it more like Danganronpa F:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make it 2.5D (3D transforms of 2D sprites).&lt;/li&gt;
&lt;li&gt;Add depth of vision effect.&lt;/li&gt;
&lt;li&gt;Add motion blur effect.&lt;/li&gt;
&lt;li&gt;Etc.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But let's focus on camera panning for now.&lt;/p&gt;
&lt;h3&gt;
  
  
  Danganronpa F tutorial
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zekoamw03v89t9zl2x9.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6zekoamw03v89t9zl2x9.jpg" alt="drf-cancel" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Danganronpa F: Cancel.&lt;/strong&gt;&lt;br&gt;
Notice that there are two cameras: A complete view of the stage and a close-up view of the active speaker (again, same stage).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One of Danganronpa F's creators made a video tutorial about it.&lt;br&gt;
They use After Effects, but their techniques can be employed in other contexts, like Web technologies.&lt;/p&gt;

&lt;p&gt;See &lt;a href="https://www.youtube.com/watch?v=iOlk6GDzS8M" rel="noopener noreferrer"&gt;Panning and Camera - Fanganronpa Tutorial (AE) | YouTube&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Essentially, they:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Set up the composition or world (background and characters).&lt;/li&gt;
&lt;li&gt;Use a camera (Camera 1) to show the entire composition, covering the entire screen.&lt;/li&gt;
&lt;li&gt;Draw a semi-transparent overlay.&lt;/li&gt;
&lt;li&gt;Use another camera (Camera 2) to show a close-up view of the composition.&lt;/li&gt;
&lt;li&gt;Update Camera 2's position (x, y, and z) to switch focus between characters... Panning.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Main layers and the main idea
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0gbiwfvfmzdp41omeej.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fx0gbiwfvfmzdp41omeej.jpg" alt="drv3-letsgo-1" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Danganronpa V3: World.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fagf9z42v0kn3khuf0pmy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fagf9z42v0kn3khuf0pmy.jpg" alt="drv3-letsgo-2" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Danganronpa V3: World + overlay.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7xl71q4sh5wrrl4ztfn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ft7xl71q4sh5wrrl4ztfn.jpg" alt="drv3-letsgo-3" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Danganronpa V3: World + overlay + close-up camera + UI.&lt;/strong&gt;&lt;br&gt;
Notice that the girl wearing a hat (Himiko) is still visible behind the overlay.&lt;/p&gt;
&lt;/blockquote&gt;


&lt;h3&gt;
  
  
  Translating concepts
&lt;/h3&gt;

&lt;p&gt;Naming things is hard. So, let's reuse terms commonly found in game dev: scene, UI, world, and camera. (These will be our variables or element IDs.)&lt;/p&gt;

&lt;p&gt;They are comparable to concepts found in game engines (e.g. Unity or Ren'Py), video editing software (e.g. After Effects), or 3D animation software (e.g. Blender).&lt;/p&gt;

&lt;p&gt;Elements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;strong&gt;scene&lt;/strong&gt; is the whole composition.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;world&lt;/strong&gt; contains "game" objects (the background, characters, items, etc.).&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;camera&lt;/strong&gt; defines a viewport or perspective on the &lt;strong&gt;world&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;UI&lt;/strong&gt; acts as an overlay on top of the world and includes textboxes and whatnot.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Pseudocode
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"scene"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"world"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"background"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"nagito"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;image&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"hajime"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;symbol&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"camera"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#world"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/symbol&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;use&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#camera"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"overlay"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;g&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ui"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"speaker"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;???&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"dialog"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;No, that's wrong!&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/g&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Impl details
&lt;/h2&gt;

&lt;p&gt;Suppose this is our &lt;strong&gt;world&lt;/strong&gt; and we want to change the camera focus from Nagito (blue rect) to Hajime (red rect):&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweti2e5r6dmlmhhy77oy.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fweti2e5r6dmlmhhy77oy.jpg" alt="Inkscape" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To animate the &lt;code&gt;symbol&lt;/code&gt;'s &lt;code&gt;viewBox&lt;/code&gt;, we could use GSAP or a similar library, but since the animation is basic, I decided to use only the &lt;code&gt;&amp;lt;animate&amp;gt;&lt;/code&gt; element.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alternating&lt;/strong&gt;: SVG does not have something similar to CSS &lt;code&gt;animation-direction: alternate&lt;/code&gt;. To recreate it, we have to define two animations and make each one start at the end of the other. To start this loop, we also set one of them (&lt;code&gt;toHajime&lt;/code&gt;) to start at 0s in the document's timeline (i.e. once the document loads).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- From Nagito to Hajime and then back to Nagito --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"toHajime"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"0s; toNagito.end"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;animate&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"toNagito"&lt;/span&gt; &lt;span class="na"&gt;begin=&lt;/span&gt;&lt;span class="s"&gt;"toHajime.end"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Delaying&lt;/strong&gt;: We want the camera to pause for a brief moment (say, 1s) at each sprite before moving to the other.&lt;br&gt;
Turns out, we can write something like &lt;code&gt;toNagito.end + 1s&lt;/code&gt; to delay the execution of the animation, relative to another event.&lt;br&gt;
We also use &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill#animate" rel="noopener noreferrer"&gt;&lt;code&gt;fill="freeze"&lt;/code&gt;&lt;/a&gt; to keep the animation's last frame's state, similar to CSS &lt;code&gt;animation-fill-mode: forwards&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2.5D or 3D perspective&lt;/strong&gt;: Sadly, SVG does not support 3D transforms. That's why everything is 2D.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom easing&lt;/strong&gt;: Check &lt;em&gt;oak&lt;/em&gt;'s post.&lt;br&gt;
Easing function used: &lt;strong&gt;easeInOutQuint&lt;/strong&gt; (&lt;code&gt;cubic-bezier(0.86,0,0.07,1)&lt;/code&gt;). See &lt;a href="https://easings.co" rel="noopener noreferrer"&gt;https://easings.co&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;???&lt;/p&gt;

&lt;p&gt;Takeaways:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transferable knowledge and whatnot.&lt;/li&gt;
&lt;li&gt;SVG is cool.&lt;/li&gt;
&lt;/ul&gt;




&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Check it live: &lt;a href="https://djalilhebal.github.io/dr-back-and-forth/svg-ver" rel="noopener noreferrer"&gt;https://djalilhebal.github.io/dr-back-and-forth/svg-ver&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Source code: &lt;a href="https://github.com/djalilhebal/dr-back-and-forth" rel="noopener noreferrer"&gt;https://github.com/djalilhebal/dr-back-and-forth&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;a href="https://www.motiontricks.com/basic-svg-viewbox-animation/" rel="noopener noreferrer"&gt;Basic SVG viewBox animation | Motion Tricks&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visited and #archived/20240202092018&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://oak.is/thinking/animated-svgs/" rel="noopener noreferrer"&gt;Animated SVGs: Custom easing and timing | oak&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Visited and #archived/20240202113846&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.com/a/33821641" rel="noopener noreferrer"&gt;Animate SVG viewBox change | Stack Overflow&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The question mentioned Velocity, but the answer suggests the &lt;code&gt;animate&lt;/code&gt; SVG element.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;a href="https://stackoverflow.com/a/31690969" rel="noopener noreferrer"&gt;SVG animation delay on each repetition | Stack Overflow&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We can append &lt;code&gt;anim.end + 2s&lt;/code&gt; (for example) in &lt;code&gt;begin&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;I don't believe &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/begin" rel="noopener noreferrer"&gt;&lt;code&gt;begin&lt;/code&gt;'s MDN page&lt;/a&gt; mentions this.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

</description>
      <category>svg</category>
    </item>
    <item>
      <title>Cropping videos with ffmpeg, but visually?</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Fri, 29 Dec 2023 17:22:32 +0000</pubDate>
      <link>https://dev.to/djalilhebal/cropping-videos-with-ffmpeg-but-visually-28o8</link>
      <guid>https://dev.to/djalilhebal/cropping-videos-with-ffmpeg-but-visually-28o8</guid>
      <description>&lt;p&gt;There are many ways to skin a cat. The same thing can be said about editing videos.&lt;/p&gt;

&lt;p&gt;This post is specifically about cropping.&lt;/p&gt;

&lt;p&gt;By the end of it, you will learn about some challenges when dealing with multimedia on the Web, how to solve or avoid them, what &lt;code&gt;ffmpeg&lt;/code&gt; and SVG can do, and how we could build a graphical video editor using these technologies.&lt;/p&gt;

&lt;p&gt;The post follows the following plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How normal people do it.&lt;/li&gt;
&lt;li&gt;My manual workflow.&lt;/li&gt;
&lt;li&gt;My attempt at improving this flow and making it "semi-automatic".&lt;/li&gt;
&lt;li&gt;Possible improvements to my attempt.&lt;/li&gt;
&lt;li&gt;Existing solutions that already implement those possible improvements.&lt;/li&gt;
&lt;li&gt;Conclusion.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Normal way
&lt;/h2&gt;

&lt;p&gt;Just use a graphical video editor like &lt;a href="https://shotcut.org" rel="noopener noreferrer"&gt;Shotcut&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manual approach
&lt;/h2&gt;

&lt;p&gt;I often use a command-line program called &lt;strong&gt;ffmpeg&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Conveniently, &lt;code&gt;ffmpeg&lt;/code&gt; has a filter called &lt;a href="https://ffmpeg.org/ffmpeg-filters.html#crop" rel="noopener noreferrer"&gt;&lt;strong&gt;&lt;code&gt;crop&lt;/code&gt;&lt;/strong&gt;&lt;/a&gt; that is used to crop stuff:&lt;br&gt;&lt;br&gt;
&lt;code&gt;ffmpeg -i in.mp4 -filter:v "crop=out_w:out_h:x:y" out.mp4&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;My usual workflow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Take a snapshot&lt;/strong&gt;:&lt;br&gt;
Using VLC's snapshot shortcut: Shift + S.&lt;br&gt;&lt;br&gt;
But I have a habit of using the following instead: Menu or right click &amp;gt; V &amp;gt; V &amp;gt; S.&lt;br&gt;&lt;br&gt;
On Windows, the snapshot will be saved in &lt;code&gt;~/Pictures/&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzql4hx6yxt1zv7u3f7dl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fzql4hx6yxt1zv7u3f7dl.png" alt="VLC - Taking a snapshot" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Define parameters&lt;/strong&gt;:&lt;br&gt;
Using Inkscape: &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Import the snapshot to a new document.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Draw a rectangle (hotkey: R).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Copy the &lt;code&gt;rect&lt;/code&gt;'s values: &lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;, &lt;code&gt;w&lt;/code&gt;, and &lt;code&gt;h&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhfgmghdxlf8a3mzaizhk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhfgmghdxlf8a3mzaizhk.png" alt="Inkscape - Drawing a rectangle" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cropping&lt;/strong&gt;:&lt;br&gt;
Using ffmpeg:&lt;br&gt;
&lt;code&gt;ffmpeg -i in.mp4 -filter:v "crop=607:1080:819:0" out.mp4&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Done&lt;/strong&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwk31k9zgqhrabv8laae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcwk31k9zgqhrabv8laae.png" alt="VLC - Showing the cropped output" width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why not use a proper (GUI) video editor?&lt;br&gt;
Laptop too shit.&lt;/p&gt;

&lt;p&gt;Also, this is not a huge issue since I hardly ever need to perform this.&lt;br&gt;
Still, I thought I might as well try to automate it... or make it "semi-automatic" at least.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semi-automatic approach
&lt;/h2&gt;

&lt;p&gt;Let's write a simple web app that helps us crop stuff using ffmpeg: &lt;em&gt;ffmpeg crop helper&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The idea is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Display an &lt;code&gt;img&lt;/code&gt; or &lt;code&gt;video&lt;/code&gt; (the &lt;strong&gt;original media&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Display a resizable and moveable overlay (the &lt;strong&gt;crop mask&lt;/strong&gt;).&lt;/li&gt;
&lt;li&gt;Generate an output based on the overlay's size and position.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dimensions problem
&lt;/h3&gt;

&lt;p&gt;Let's consider an example with specific dimensions to illustrate a possible challenge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Original image dimensions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Original image width: 1000 pixels&lt;/li&gt;
&lt;li&gt;Original image height: 800 pixels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Transformation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The image is displayed on a webpage and is scaled down by 50%.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;New image dimensions:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scaled image width: 500 pixels (50% of 1000)&lt;/li&gt;
&lt;li&gt;Scaled image height: 400 pixels (50% of 800)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, let's assume we want to define a crop area on this scaled image using traditional coordinate systems without considering scaling.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Traditional coordinates:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We want to crop a rectangular area starting from (100, 100) and with dimensions (300 x 200).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Challenge:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In the original coordinate system, the starting point (100, 100) refers to a location on the original image with dimensions (1000 x 800).&lt;/li&gt;
&lt;li&gt;However, when the image is scaled down, the coordinate (100, 100) on the scaled image does not correspond to the same absolute location on the original image.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Meaning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If we use the same coordinates without considering the scaling, our crop area will be misplaced, and the dimensions won't be accurate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also, keep in mind the size of the element (image in our example) may change dynamically due to the environment: screen rotation, styles added, etc.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
We need to convert values between coordinate systems. Geometry problem. Math. I can't be bothered with that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Actual solution:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Instead, let's keep everything (the image and crop mask) in a shared coordinate system so we don't have to convert anything.&lt;br&gt;
It should be able to scale freely.&lt;br&gt;
That description kinda feels like SVG and its &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/viewBox" rel="noopener noreferrer"&gt;&lt;code&gt;viewBox&lt;/code&gt; attribute&lt;/a&gt;. &lt;strong&gt;SVG is our canvas.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;As for the original media, we can use the &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/foreignObject" rel="noopener noreferrer"&gt;&lt;code&gt;foreignObject&lt;/code&gt;&lt;/a&gt; SVG tag to embed an HTML &lt;code&gt;video&lt;/code&gt; or &lt;code&gt;img&lt;/code&gt; element. &lt;strong&gt;This is the original media.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;rect&lt;/code&gt;angle can be used to define &lt;strong&gt;the crop area.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Using SVG satisfies the need for a consistent coordinate system. With &lt;code&gt;viewBox&lt;/code&gt;, we can define the coordinate system based on the original image dimensions, even when the image is scaled or transformed, ensuring accurate and consistent positioning of elements within the SVG canvas.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;UX&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;There is no building way to make an element (HTML or SVG) easily resizable and moveable. And, no, &lt;a href="https://developer.mozilla.org/en-US/docs/Web/CSS/resize" rel="noopener noreferrer"&gt;CSS &lt;code&gt;resize&lt;/code&gt; attribute&lt;/a&gt; does not help much.&lt;/p&gt;

&lt;p&gt;To build a decent UI, we need to create different event handlers (like at corners) and manage different mouse/pointer events.&lt;br&gt;&lt;br&gt;
I've done something similar in Java (Swing); it's bothersome and hard to get right; I don't wanna redo it again, so a crude solution will have to do... for now.&lt;/p&gt;

&lt;p&gt;Let's just display various inputs and let the user change them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pseudocode
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
    Original file:
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"file"&lt;/span&gt; &lt;span class="na"&gt;accept=&lt;/span&gt;&lt;span class="s"&gt;"image/*,video/*"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;

    Crop area:
    Width &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"$out_w"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"$orig_width"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    Height &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"$out_h"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"$orig_height"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    X &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"$out_x"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"$orig_width"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    Y &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"$out_y"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"number"&lt;/span&gt; &lt;span class="na"&gt;min=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;max=&lt;/span&gt;&lt;span class="s"&gt;"$orig_height"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 $orig_width $orig_height"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;foreignObject&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crop-object"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt; or &lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/foreignObject&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"crop-mask"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;output&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;code&amp;gt;&lt;/span&gt;
    ffmpeg
        -i in.mp4
        &lt;span class="nt"&gt;&amp;lt;b&amp;gt;&lt;/span&gt;-filter:v "crop=$out_w:$out_h:$out_x:$out_y"&lt;span class="nt"&gt;&amp;lt;/b&amp;gt;&lt;/span&gt;
        out.mp4
    &lt;span class="nt"&gt;&amp;lt;/code&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/output&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Some implementation details
&lt;/h3&gt;

&lt;p&gt;Some implementation details (or problems) worth mentioning:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How do we know when the media has loaded?&lt;/li&gt;
&lt;li&gt;How to get the original media's dimensions (intrinsic width and height)?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For &lt;strong&gt;img&lt;/strong&gt;: &lt;code&gt;load&lt;/code&gt; event fires whenever the &lt;code&gt;src&lt;/code&gt; resource loads.&lt;br&gt;
It fires even if we set &lt;code&gt;img.src = img.src&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For &lt;strong&gt;video&lt;/strong&gt;: &lt;code&gt;loadedmetadata&lt;/code&gt; event when the video's metadata is loaded. &lt;em&gt;This includes its dimensions.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Intrinsic dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;For &lt;strong&gt;img&lt;/strong&gt;: &lt;code&gt;.naturalWidth&lt;/code&gt; and &lt;code&gt;.naturalHeight&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For &lt;strong&gt;video&lt;/strong&gt;: &lt;code&gt;.videoWidth&lt;/code&gt; and &lt;code&gt;.videoHeight&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Why couldn't both &lt;strong&gt;img&lt;/strong&gt; and &lt;strong&gt;video&lt;/strong&gt; properties be called &lt;code&gt;intrinsicWidth&lt;/code&gt; and &lt;code&gt;intrinsicHeight&lt;/code&gt;? Web things, I guess.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rough implementation
&lt;/h3&gt;

&lt;p&gt;I built a rough implementation in Vue.&lt;/p&gt;

&lt;p&gt;It works as described above and supports both images and videos.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nwnlbaswfmocaccjlma.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2nwnlbaswfmocaccjlma.png" alt="Selected an image" width="800" height="391"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16dq1j95udud0utfnjsm.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F16dq1j95udud0utfnjsm.png" alt="Selected a video" width="800" height="393"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Possible improvements&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resize and move the crop area using a pointer (mouse).&lt;/li&gt;
&lt;li&gt;Preview the output.&lt;/li&gt;
&lt;li&gt;Drag and drop files.&lt;/li&gt;
&lt;li&gt;Define presets (like common aspect ratios).&lt;/li&gt;
&lt;li&gt;&lt;del&gt;Automatically determine the file type (image or video).&lt;/del&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's unlikely that I implement them anytime soon. After all, this was just a proof-of-concept.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What concept?&lt;/em&gt; You ask. I'll answer that in a bit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Existing solutions
&lt;/h2&gt;

&lt;p&gt;There exist many projects that address the mentioned "possible improvements".&lt;/p&gt;

&lt;p&gt;Other than dedicated video editors, some open-source projects serve as graphical user interfaces for ffmpeg:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/Nemo64/clip" rel="noopener noreferrer"&gt;Marco's clip&lt;/a&gt; is my favorite by far.&lt;br&gt;&lt;br&gt;
It also supports clipping, and compression, provides presets (aspect ratios), and allows the user to drag and drop the video file. And it works entirely in the browser.&lt;br&gt;
Overall, its UX is epic.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Another one is Manfred's &lt;a href="https://github.com/jibbex/jib-FFmpeg" rel="noopener noreferrer"&gt;jib-FFmpeg&lt;/a&gt;, which is described as "&lt;em&gt;Yet another graphical user interface for FFmpeg&lt;/em&gt;".&lt;br&gt;
I haven't tried it, but it seems unmaintained and depends on &lt;code&gt;react-image-crop&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/DominicTobias/react-image-crop" rel="noopener noreferrer"&gt;&lt;code&gt;react-image-crop&lt;/code&gt;&lt;/a&gt; and similar components let you define the cropping area for images, videos, or other elements.&lt;br&gt;
It handles the conversion between coordinate systems (not sure about layout shifts).&lt;br&gt;
We could use it.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Takeaway:&lt;/strong&gt; There are many ways to solve a problem.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytzmfpkkrm3eql0szc49.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fytzmfpkkrm3eql0szc49.png" alt="Alice to Cheshire Cat: And if not, there may be more than one way to skin a cat, if you'll pardon the expression." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, FFmpeg and SVG are cool. JavaScript, not so much.&lt;/p&gt;

&lt;p&gt;The goal of this project was to test the concept of using SVG to avoid coordinate transformation challenges while creating a video editor. The proof-of-concept works as expected.&lt;/p&gt;

&lt;p&gt;If you are curious, you can check it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://ffmpeg-crop-helper.vercel.app" rel="noopener noreferrer"&gt;https://ffmpeg-crop-helper.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Source code: &lt;a href="https://github.com/djalilhebal/ffmpeg-crop-helper" rel="noopener noreferrer"&gt;https://github.com/djalilhebal/ffmpeg-crop-helper&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ffmpeg</category>
      <category>svg</category>
      <category>vue</category>
    </item>
    <item>
      <title>Pronouncing names using TTS</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Tue, 12 Dec 2023 19:50:00 +0000</pubDate>
      <link>https://dev.to/djalilhebal/pronouncing-names-and-stuff-using-tts-5a81</link>
      <guid>https://dev.to/djalilhebal/pronouncing-names-and-stuff-using-tts-5a81</guid>
      <description>&lt;p&gt;I'm Abdeldjalil. Go ahead, try saying my name. You probably don't know how to pronounce it, and that's fine.&lt;/p&gt;

&lt;p&gt;Let's consider the diverse world of names out there.&lt;br&gt;
Some are straightforward like Alice, while others might need a bit of explaining, like "Djalil, but the 'D' is silent".&lt;br&gt;
And then there are those complex ones like Wriothesley. Wikipedia might tell you it's pronounced &lt;code&gt;/ˈraɪəθsli/&lt;/code&gt; (RYE-thes-lee), Genshin Impact might suggest &lt;code&gt;/ˈraɪzli/&lt;/code&gt; (RYZE-lee), but others just call him &lt;code&gt;/ˈraɪ.oʊ/&lt;/code&gt; (RAI-oh).&lt;br&gt;
&lt;a href="https://www.youtube.com/watch?v=kdQOis0VRoo" rel="noopener noreferrer"&gt;Wrio is super omega cool&lt;/a&gt;, don't you agree? &lt;a href="https://www.youtube.com/watch?v=dq3Vd3LZTUM" rel="noopener noreferrer"&gt;Just look at him!&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's refocus—&lt;/p&gt;

&lt;p&gt;I'm the kind of person who loves diving into new concepts and words. Even when I'm offline, I rely on dictionaries (physical or some Wiktionary-based app) as my go-to resource to learn new terms, and this habit propelled me into learning IPA transcription to pronounce them properly.&lt;br&gt;
Look at me, trying to romanticize a simple project that helps people pronounce names and stuff.&lt;/p&gt;

&lt;p&gt;Let's just get into it.&lt;/p&gt;



&lt;p&gt;
  Honestly?
  &lt;br&gt;
Why make this thing?

&lt;p&gt;To showcase that I can use cloud-y stuff like AWS or Google Cloud.&lt;/p&gt;

&lt;p&gt;Ended up using Vercel and Azure because I ain't paying no money for this thing, but I want it to work "forever".&lt;/p&gt;

&lt;p&gt;I even resorted to using Next.js when a simple page would do.&lt;/p&gt;

&lt;p&gt;The things we do for recognition. :p&lt;br&gt;
&lt;/p&gt;

&lt;br&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Naming it
&lt;/h2&gt;

&lt;p&gt;This is the most important step—coming up with a name that captures the project's essence.&lt;/p&gt;

&lt;p&gt;The goal was to make an &lt;em&gt;IPA vocalizer&lt;/em&gt; with a focus on pronouncing names:&lt;br&gt;
A name's pronunciation, vocalization, or sound = Name + Sound = &lt;em&gt;Namae&lt;/em&gt; (Japanese for "name") + &lt;em&gt;Ne&lt;/em&gt; (Japanese for "sound") = &lt;strong&gt;&lt;em&gt;Namaene&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The name also draws parallels to Vocaloid, a singing voice synthesizer, and specifically characters like Miku &lt;strong&gt;Hatsune&lt;/strong&gt; (First + Sound) and Len &lt;strong&gt;Kagamine&lt;/strong&gt; (Mirror + Sound).&lt;/p&gt;

&lt;p&gt;Perfect. Now, we just need to make it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmsmdtm17w6avpv5wm35.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxmsmdtm17w6avpv5wm35.png" alt="Namaene" width="800" height="865"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Building it
&lt;/h2&gt;

&lt;p&gt;The premise is simple: Tell the TTS engine to read the IPA text and return the audio file to the user.&lt;/p&gt;
&lt;h3&gt;
  
  
  Synthesizing
&lt;/h3&gt;

&lt;p&gt;Speech Synthesis Markup Language (SSML) is an XML-based markup language that lets you adjust text-to-speech output attributes such as pitch, pronunciation, speaking rate, and more.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;&amp;lt;phoneme&amp;gt;&lt;/code&gt; tag accepts pronunciation transcription in IPA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;phoneme&lt;/span&gt; &lt;span class="na"&gt;alphabet=&lt;/span&gt;&lt;span class="s"&gt;"ipa"&lt;/span&gt; &lt;span class="na"&gt;ph=&lt;/span&gt;&lt;span class="s"&gt;"ˈraɪzli"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Wriothesley&lt;span class="nt"&gt;&amp;lt;/phoneme&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since we are specifying the word's pronunciation, we do not need to include it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;phoneme&lt;/span&gt; &lt;span class="na"&gt;alphabet=&lt;/span&gt;&lt;span class="s"&gt;"ipa"&lt;/span&gt; &lt;span class="na"&gt;ph=&lt;/span&gt;&lt;span class="s"&gt;"ˈraɪzli"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/phoneme&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can go further. Since the XML element contains no children, we can use a self-closing tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;  &lt;span class="nt"&gt;&amp;lt;phoneme&lt;/span&gt; &lt;span class="na"&gt;alphabet=&lt;/span&gt;&lt;span class="s"&gt;"ipa"&lt;/span&gt; &lt;span class="na"&gt;ph=&lt;/span&gt;&lt;span class="s"&gt;"ˈraɪzli"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's imagine we've written a &lt;code&gt;generateSsml(ipa)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Next, we will call some TTS engine or service to speak the SSML.&lt;/p&gt;

&lt;p&gt;Return the audio data ("speech production") with appropriate headers (mainly &lt;code&gt;Content-Type&lt;/code&gt;), and let the client (e.g. browser) deal with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing a TTS service
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Requirements
&lt;/h3&gt;

&lt;p&gt;This is what we are looking for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Buzzwordy enough. It should be something listable on a LinkedIn Skills section (serverless, distributed, frameworks, blockchain, and can I fit WordPress somewhere?).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Free&lt;/strong&gt;. We are not willing to pay anything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Forever&lt;/strong&gt;. It should work "forever".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;IPA&lt;/strong&gt; support, probably via &lt;strong&gt;SSML&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Options
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Amazon Web Service (AWS)&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSML? Yes.

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/polly/latest/dg/ssml.html" rel="noopener noreferrer"&gt;Generating Speech from SSML Documents | Amazon Polly&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Free? Not really. Only "12 months free", &lt;strong&gt;not&lt;/strong&gt; "free forever".&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Microsoft Azure&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSML? Yes.

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/speech-synthesis-markup-pronunciation" rel="noopener noreferrer"&gt;Pronunciation with Speech Synthesis Markup Language (SSML) - Speech service - Azure AI services | Microsoft Learn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;Free? Yes, but is it enough?

&lt;ul&gt;
&lt;li&gt;Text to Speech / 0.5 million characters free per month.
&lt;a href="https://azure.microsoft.com/en-us/pricing/details/cognitive-services/speech-services/" rel="noopener noreferrer"&gt;Speech Services Pricing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Google Cloud&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SSML? Yes.&lt;/li&gt;
&lt;li&gt;Free? Yes, enough.

&lt;ul&gt;
&lt;li&gt;Free per month / Standard voices / 0 to 4 million characters (&lt;a href="https://cloud.google.com/text-to-speech/pricing" rel="noopener noreferrer"&gt;https://cloud.google.com/text-to-speech/pricing&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Also, "new customers get $300 in free credits to spend on Text-to-Speech." (&lt;a href="https://cloud.google.com/text-to-speech/" rel="noopener noreferrer"&gt;https://cloud.google.com/text-to-speech/&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;In short, AWS seems cool and I like its voices (even since they were called Ivona), but I needed something that's more future-proof.&lt;/p&gt;

&lt;p&gt;Both Microsoft and Google are fine, but having 4M characters per month seems more appealing than 0.5M.&lt;br&gt;
&lt;a href="https://github.com/GoogleCloudPlatform/nodejs-docs-samples/blob/73de2e8035eab700cdb61334456c1fec683187e3/texttospeech/synthesize.js#L42" rel="noopener noreferrer"&gt;Checking Google Cloud's TTS samples&lt;/a&gt;, I even liked their SDK (all request config in one object + you can just &lt;code&gt;await&lt;/code&gt; the response).&lt;/p&gt;

&lt;p&gt;So, Google Cloud it is... Or not.&lt;/p&gt;

&lt;p&gt;For some reason, my credit card got declined, so I resorted to using Microsoft's services since I already had an account.&lt;/p&gt;

&lt;p&gt;So, for now, Azure it is.&lt;/p&gt;
&lt;h3&gt;
  
  
  Using the service
&lt;/h3&gt;

&lt;p&gt;In a perfect world, there would be a simple &lt;code&gt;async function speakSsml(options)&lt;/code&gt; that resolves to the audio data as a &lt;code&gt;Blob&lt;/code&gt; or an &lt;code&gt;ArrayBuffer&lt;/code&gt;.&lt;br&gt;
Google Cloud SDK does something similar to what we want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textToSpeech&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google-cloud/text-to-speech&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;synthesizeSsml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ssml&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;textToSpeech&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TextToSpeechClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;ssml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ssml&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;voice&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;languageCode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en-US&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ssmlGender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FEMALE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;audioConfig&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;audioEncoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;MP3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synthesizeSpeech&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// Do something with the audio data: response.audioContent&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While in Azure's SDK, we need to wrap the call in a &lt;code&gt;Promise&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;microsoft-cognitiveservices-speech-sdk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;synthesizeSsml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ssmlText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;speechConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SpeechConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromSubscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SPEECH_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SPEECH_REGION&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechSynthesisVoiceName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en-US-JennyNeural&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;speechSynthesisOutputFormat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SpeechSynthesisOutputFormat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Ogg24Khz16BitMonoOpus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;synthesizer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;SpeechSynthesizer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;speechConfig&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;synthesizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;speakSsmlAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ssmlText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;sdk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResultReason&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SynthesizingAudioCompleted&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;audioData&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;errorDetails&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="nf"&gt;function &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Decent.&lt;/p&gt;

&lt;p&gt;Now, let's imagine that the backend framework we will be using uses Fetch API interfaces, namely &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Request" rel="noopener noreferrer"&gt;&lt;code&gt;Request&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Response" rel="noopener noreferrer"&gt;&lt;code&gt;Response&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
That would be awesome since &lt;code&gt;Response&lt;/code&gt; accepts a body of type &lt;code&gt;ArrayBuffer&lt;/code&gt;, which is convenient for our use case.&lt;/p&gt;

&lt;p&gt;A handler for &lt;code&gt;GET /api/speak/?ipa={ipa}&lt;/code&gt; can be written like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ipa&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;searchParams&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ipa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ssml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generateSsml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ipa&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;speakSsml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ssml&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;audio/ogg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;contentType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We added a &lt;code&gt;Content-Type&lt;/code&gt; header to help clients handle the response correctly.&lt;/p&gt;

&lt;p&gt;Its value is set to &lt;code&gt;audio/ogg&lt;/code&gt; because that's what we configured the SDK to return. By default, it uses some &lt;code&gt;wav&lt;/code&gt; format, but you can change it to a few other formats like &lt;code&gt;mp3&lt;/code&gt; or &lt;code&gt;ogg&lt;/code&gt;. Both of the mentioned alternatives use less space (they are compressed) and offer good quality. From my testing, &lt;code&gt;ogg&lt;/code&gt; seems a bit better in terms of quality and size. But it ultimately does not matter whichever you pick.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching
&lt;/h2&gt;

&lt;p&gt;Speaking of headers and optimization, to improve the user experience (latency) and in order not to exceed our quota, we can tell the client it should cache the result forever.&lt;/p&gt;

&lt;p&gt;Let's add a &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control" rel="noopener noreferrer"&gt;Cache-Control&lt;/a&gt; header to the response.&lt;br&gt;
The following says: Anyone (&lt;code&gt;public&lt;/code&gt;) can cache it; it stays fresh for 1 year (&lt;code&gt;31536000&lt;/code&gt; seconds); no need to revalidate since it won't change (&lt;code&gt;immutable&lt;/code&gt;) while it's fresh".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+    'Cache-Control': 'public, max-age=31536000, immutable',
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Handling quota
&lt;/h2&gt;

&lt;p&gt;Caching doesn't solve our issue. We &lt;em&gt;must&lt;/em&gt; not exceed the quota.&lt;/p&gt;

&lt;p&gt;For all providers I checked (meaning Azure and Google Cloud), &lt;em&gt;you&lt;/em&gt; need to ensure you don't go above the free monthly quota!&lt;/p&gt;

&lt;p&gt;Their Monitoring and Billing APIs are not good enough: We can (and probably will) be notified to pause our usage &lt;strong&gt;after&lt;/strong&gt; we exceed the quota. We ain't paying nothing, not a single cent.&lt;br&gt;
(For example, see &lt;a href="https://cloud.google.com/billing/docs/how-to/notify#cap_disable_billing_to_stop_usage" rel="noopener noreferrer"&gt;Google's guide about stopping billing&lt;/a&gt;: "There is a delay between incurring costs and receiving budget notifications.")&lt;/p&gt;

&lt;p&gt;We need to implement our own quota tracker.&lt;/p&gt;
&lt;h3&gt;
  
  
  What to count
&lt;/h3&gt;

&lt;p&gt;Calculating the cost of each call differs from provider to provider, and even from service to service.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tags&lt;/strong&gt;: Some SSML elements (e.g. &lt;code&gt;speak&lt;/code&gt; and &lt;code&gt;mark&lt;/code&gt;) are omitted.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Whitespace&lt;/strong&gt; is usually counted.&lt;/p&gt;

&lt;p&gt;But what about the &lt;strong&gt;characters&lt;/strong&gt;?&lt;/p&gt;

&lt;p&gt;Azure specifically counts each Chinese character as 2 chars. Google Cloud says it counts each Japanese character as 1 character.&lt;br&gt;
But what does that even mean?&lt;/p&gt;

&lt;p&gt;Consider: The Japanese word for love is 愛, pronounced &lt;em&gt;Ai&lt;/em&gt;, as in &lt;em&gt;Ai Kotoba&lt;/em&gt; (&lt;em&gt;Love Words&lt;/em&gt; feat. Hatsune Miku).&lt;br&gt;
How many characters are there?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript encodes strings in UTF-16. The word's &lt;code&gt;length&lt;/code&gt; is 1.&lt;/li&gt;
&lt;li&gt;Azure says Chinese characters (and other multi-code ones) count as 2, so the word's cost is 2.&lt;/li&gt;
&lt;li&gt;Google Cloud says each Japanese character is considered one character. Only hiragana and katakana? Does that include kanji ("Chinese characters")? Does 愛 count as 1 character?&lt;/li&gt;
&lt;li&gt;愛 is a multi-byte character. Specifically, it is encoded in 3 bytes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What about emojis if the user decides to send them for whatever reason?&lt;/p&gt;

&lt;p&gt;The safest bet is to use the SSML text's length in bytes as character count, as Google Cloud docs suggest ("[...] the number of characters will be equal to or less than the number of bytes represented by the text.").&lt;/p&gt;

&lt;p&gt;We could optimize this, but, for now, let's play it safe.&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/TextEncoder/encode" rel="noopener noreferrer"&gt;&lt;code&gt;TextEncoder&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;countCharacters&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ssmlText&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textEncoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextEncoder&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="c1"&gt;// returns Uint8Array&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textEncoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ssmlText&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;encoded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where to keep track of the count
&lt;/h3&gt;

&lt;p&gt;One thing is certain: We cannot* be sure how requests are going to be handled.&lt;br&gt;
&lt;small&gt;* Or should not or do not want to bother with.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Namaene may be deployed on some distributed, horizontally scalable platform. Or it might be running locally: Node executes requests concurrently.&lt;br&gt;
Or both. You know, production vs dev.&lt;/p&gt;

&lt;p&gt;In short, we need a way to keep track of the counter between isolated processes. Think: Transactions or similar atomic operations.&lt;/p&gt;

&lt;p&gt;Redis sounds like a good solution.&lt;/p&gt;

&lt;p&gt;Whena request arrives, &lt;code&gt;generateSsml&lt;/code&gt;, &lt;code&gt;countCharacters(/* in the */ ssmlText)&lt;/code&gt;, then increment the counter like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INCRBY totalCount requestCount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Resetting the counter
&lt;/h3&gt;

&lt;p&gt;The monthly quota resets... every... month.&lt;/p&gt;

&lt;p&gt;We need to reset our counter.&lt;br&gt;
There are multiple options to do it, including:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A &lt;code&gt;cron&lt;/code&gt; job.&lt;/li&gt;
&lt;li&gt;Redis transactions.&lt;/li&gt;
&lt;li&gt;Redis functions or scripts.&lt;/li&gt;
&lt;li&gt;Do not reset anything.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last option is appealing because it seems simple and overcomes some limitations:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;INCRBY&lt;/code&gt; is op.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;INCRBY&lt;/code&gt; has built-in handling of non-existent Keys.&lt;br&gt;
This behavior can simplify the initialization process, especially when dealing with a new month.&lt;/p&gt;

&lt;p&gt;It deals with overflows. The command will fail if its execution would cause an overflow.&lt;/p&gt;

&lt;p&gt;All of this means we do not need to &lt;code&gt;GET&lt;/code&gt; the current value and then increment it (a la &lt;em&gt;double-checked locking&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;Spoiler for the next section: Vercel KV (free/Hobby) also has a quota of 30k requests per month. What does "request" mean?... Let's not get into that.&lt;br&gt;
At any rate, the fewer commands we use, the better.&lt;/p&gt;

&lt;p&gt;Just use a different key each month (e.g. "counter:yyyy-mm"):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getCurrentCountKey&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// "The timezone is always UTC" - toISOString on MDN&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nowIso&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;yearMonth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nowIso&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;yyyy-mm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;counterKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`counter:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;yearMonth&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;counterKey&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;INCRBY counter:2023-12 500&lt;/code&gt; increments the counter by 500 and then returns the new value.&lt;br&gt;
Next month, the counter key will automatically change to &lt;code&gt;counter:2024-01&lt;/code&gt;. &lt;code&gt;INCRBY&lt;/code&gt; will not find this key, so it will be set to 0 before incrementing.&lt;/p&gt;

&lt;p&gt;Having decided on this &lt;del&gt;lazy&lt;/del&gt; practical approach, we can think of it as an opportunity to implement another important feature: Maintaining a historical record of monthly usage/counts.&lt;/p&gt;

&lt;p&gt;To get a list of all counter keys (then iterate over them):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;KEYS counter:*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A better approach might be to use hash commands: &lt;a href="https://redis.io/commands/hincrby/" rel="noopener noreferrer"&gt;&lt;code&gt;HINCRBY&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://redis.io/commands/hgetall/" rel="noopener noreferrer"&gt;&lt;code&gt;HGETALL&lt;/code&gt;&lt;/a&gt;.&lt;br&gt;
This is not of high priority.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying
&lt;/h3&gt;

&lt;p&gt;Having already tried Netlify (in 2020) and Vercel (recently), I decided to use Vercel mostly because I like their website's design—&lt;/p&gt;

&lt;p&gt;I mean the UX and DX (docs, APIs, and all). Plus, they provide or are behind some of the technologies we &lt;del&gt;wanted to&lt;/del&gt; use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Serverless Redis&lt;/li&gt;
&lt;li&gt;Serverless functions (e.g. the &lt;code&gt;/api/speak&lt;/code&gt; handler)&lt;/li&gt;
&lt;li&gt;Next.js (it uses &lt;a href="https://nextjs.org/docs/pages/api-reference/functions/next-server" rel="noopener noreferrer"&gt;Fetch API-based classes&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is a rough sketch of what we ended up with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GET /api/speak?voice={voice}&amp;amp;ipa={ipa}&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7sx175dc5gtziwm7w53.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ff7sx175dc5gtziwm7w53.png" alt="System arch overview" width="800" height="263"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This was a simplified overview of the project and some of the technical decisions I had to make.&lt;/p&gt;

&lt;p&gt;It is far from perfect, but... I'm learning.&lt;/p&gt;

&lt;p&gt;Feel free to explore the &lt;a href="https://github.com/djalilhebal/namaene" rel="noopener noreferrer"&gt;&lt;strong&gt;project's repo&lt;/strong&gt;&lt;/a&gt; or &lt;a href="https://namaene.vercel.app/" rel="noopener noreferrer"&gt;&lt;strong&gt;try it live&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Thanks for reading.&lt;/p&gt;

&lt;p&gt;— Djalil (PS: The D is silent.)&lt;/p&gt;

&lt;p&gt;&lt;em&gt;その名前をさあ、言ってごらん このぼくの名前を！&lt;/em&gt;&lt;/p&gt;

</description>
      <category>vercel</category>
      <category>azure</category>
      <category>texttospeech</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Experiment: Playing a non-English visual novel through Google Translate's camera</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Sat, 30 May 2020 21:41:25 +0000</pubDate>
      <link>https://dev.to/djalilhebal/experiment-playing-a-non-english-visual-novel-through-google-translate-s-camera-8mi</link>
      <guid>https://dev.to/djalilhebal/experiment-playing-a-non-english-visual-novel-through-google-translate-s-camera-8mi</guid>
      <description>&lt;p&gt;I'm a big fan of a video game series called &lt;em&gt;Danganronpa&lt;/em&gt;--a Japanese visual novel about &lt;strong&gt;Hope&lt;/strong&gt; and &lt;strong&gt;Despair&lt;/strong&gt;, and a "robotic bear" forcing students to kill one another.&lt;/p&gt;

&lt;p&gt;The other day I came across a trailer for a game which I was hooked on right away because, as Reddit user &lt;a href="https://www.reddit.com/r/danganronpa/comments/grgj62/are_you_familiar_with_wasabi_studio_games_despair/frypwks?utm_source=share&amp;amp;utm_medium=web2x" rel="noopener noreferrer"&gt;u/Little-Big-Smoke commented&lt;/a&gt;,&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Have to admit, that trailer indeed screams "Neon Danganronpa"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After doing some research, I learned that it's called &lt;em&gt;Zetsubou Prison&lt;/em&gt; (Despair Prison), developed by &lt;em&gt;Studio Wasabi&lt;/em&gt;, and is available for Android, iOS, and Windows but only in Japanese and Chinese.&lt;/p&gt;

&lt;p&gt;I desperately wanted to play it... and that's what I've done.&lt;/p&gt;

&lt;p&gt;This post tries to follow this structure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How I actually played it&lt;/li&gt;
&lt;li&gt;What I tried&lt;/li&gt;
&lt;li&gt;Two ideas for automating the process&lt;/li&gt;
&lt;li&gt;Some conclusions&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How I played it
&lt;/h2&gt;

&lt;p&gt;Being a Linux user (Kubuntu v18.4), I ended up downloading Steam and using Steam Play (&lt;a href="https://github.com/ValveSoftware/Proton" rel="noopener noreferrer"&gt;Proton&lt;/a&gt; v5) to run the game on PC.&lt;/p&gt;

&lt;p&gt;For some reason, the Steam version provides only the Chinese version.&lt;/p&gt;

&lt;p&gt;Anyway, I used Google Translate's &lt;em&gt;instant camera translation&lt;/em&gt; functionality (on my Android phone) to read the screen. (One could say I played "through the looking glass." #funny 🤦)&lt;/p&gt;

&lt;p&gt;Google Translate does a surprisingly well job in translating from Chinese to English.&lt;br&gt;
A few translation mistakes were obvious but I "mentally fixed them" from the context.&lt;br&gt;&lt;br&gt;
Although Chinese and Japanese use the same characters (&lt;em&gt;kanji&lt;/em&gt;) to write names, they differ in pronunciation. This is why I sometimes would switch to Japanese to learn the characters' names and then switch back to Chinese to continue reading dialogues.&lt;/p&gt;

&lt;p&gt;And that's how I played the first chapter of Despair Prison... like a savage... and I enjoyed it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Screenshots
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fj39pilacqj8fon6da9o9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fj39pilacqj8fon6da9o9.png" alt="Screenshot" width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;&lt;br&gt;
Precious bois, from left to right: Rui, Shiro, and Kisuke.&lt;br&gt;&lt;br&gt;
Shiro: "Huh? What did you recall/think of?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;

  &lt;tr&gt;
    &lt;td&gt;&lt;img alt="Screenshot" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fyo8wyi63kcm94b6bpikk.jpg" width="720" height="1480"&gt;&lt;/td&gt;
    &lt;td&gt;&lt;img alt="Screenshot" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F8cgzawu4zbulhcd5u5cn.jpg" width="720" height="1480"&gt;&lt;/td&gt;
    &lt;td&gt;&lt;img alt="Screenshot" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F41x8o5vmvv39pzq6joz7.jpg" width="720" height="1480"&gt;&lt;/td&gt;
  &lt;/tr&gt;

  &lt;tr&gt;
    &lt;th&gt;Shiro's introduction: "I'm Fuyutsuki Shiro! I look forward to your guidance/advice."&lt;/th&gt;
    &lt;th&gt;Shiro's profile: He seems like a &lt;em&gt;best boi&lt;/em&gt; but a little suspicious.&lt;/th&gt;
    &lt;th&gt;Shiro's enjoying the situation&lt;/th&gt;
  &lt;/tr&gt;

&lt;/table&gt;&lt;/div&gt;


&lt;h2&gt;
  
  
  Tried: Screen translator apps
&lt;/h2&gt;

&lt;p&gt;On Android.&lt;/p&gt;

&lt;p&gt;Of the screen translator apps I tried, &lt;a href="https://play.google.com/store/apps/details?id=com.tranit.text.translate&amp;amp;hl=en" rel="noopener noreferrer"&gt;&lt;em&gt;Tranit&lt;/em&gt;&lt;/a&gt; was the most promising one and which had the nicest UX.&lt;/p&gt;

&lt;p&gt;Unfortunately, it didn't work in this game.&lt;/p&gt;

&lt;p&gt;Since Tranit used Android accessibility features to read/translate the screen, I tried to use &lt;a href="https://play.google.com/store/apps/details?id=com.deque.axe.android&amp;amp;hl=en" rel="noopener noreferrer"&gt;&lt;em&gt;axe for Android&lt;/em&gt;&lt;/a&gt; to "debug" it but &lt;em&gt;axe&lt;/em&gt; couldn't detect anything. I assume the game developers did what's equivalent to drawing using the &lt;em&gt;Canvas API&lt;/em&gt; instead of using native and semantic elements.&lt;/p&gt;


&lt;h2&gt;
  
  
  Idea: Doppelganger.js
&lt;/h2&gt;

&lt;p&gt;Why not recreate the "instant camera translation" thingie on the desktop?&lt;br&gt;&lt;br&gt;
This should be doable using Screen Capture API, Google Vision and Translate APIs, Electron, and maybe &lt;a href="https://github.com/octalmage/robotjs" rel="noopener noreferrer"&gt;RobotJS&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This works best for Japanese and Chinese visual novels and text-focused JRPGs with sparse animations and non-animated sprites.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="cm"&gt;/**
 * Doppelganger.js
 * As in those doppelgangers we find in Despair Prison
 * Pseudo-code for a program that adds a live translation overlay for the desktop
 */&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;electron&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;electron&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;robot&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;robotjs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Vision&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google-cloud/vision&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Translate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@google-cloud/translate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toEnglish&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;Translate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// The overlay shoud support input forwarding (i.e. mouse and keyboard events)&lt;/span&gt;
&lt;span class="c1"&gt;// either manually (listen for these events and emulate them in the original window using RobotJS)&lt;/span&gt;
&lt;span class="c1"&gt;// or hopefully automatically (see https://www.electronjs.org/docs/api/frameless-window )&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;overlayWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;electron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;transparent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Actually, only update the overlay if there is a change in the capture stream&lt;/span&gt;
&lt;span class="c1"&gt;// this is better for performace and to respect the Google's limitations&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Or maybe use https://developer.mozilla.org/en-US/docs/Web/API/Screen_Capture_API&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;capture&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;robot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;textAnnotations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Vision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;annotateImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;capture&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;translatedAnnotations&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;textAnnotations&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toEnglish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;

  &lt;span class="c1"&gt;// Update the "doppelganger" window's DOM with text elements that "overlay" on the original texts&lt;/span&gt;
  &lt;span class="c1"&gt;// (with the help of annotation's metadata like the bounding box)&lt;/span&gt;
  &lt;span class="nf"&gt;updateOverlay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;overlayWindow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;translatedAnnotations&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Idea: Emulate the Google Translate app
&lt;/h2&gt;

&lt;p&gt;Trick the Google Translate app into translating the screen for us:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/umlaeute/v4l2loopback/" rel="noopener noreferrer"&gt;Configure the operating system to allow creating "virtual video devices" (the v4l2loopback project on GitHub)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://stackoverflow.com/q/25396784" rel="noopener noreferrer"&gt;FFmpeg: Record a specific window (question on Stack Overflow)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://superuser.com/a/713100" rel="noopener noreferrer"&gt;FFmpeg: Use the desktop (or just that specific window) as a "fake webcam" (answer on Super User)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://stackoverflow.com/a/30792615" rel="noopener noreferrer"&gt;Android Studio: Set that "fake webcam" as the camera in the Android emulator (answer on Stack Overflow)&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install Google Translate in the Android emulator (as a tablet or TV device, to have a large screen).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Open the game and Android emulator windows side by side.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Or open them on different monitors (if you have).&lt;/li&gt;
&lt;li&gt;Or maybe turn the emulator window into an "overlay" (as in Doppelganger.js) by making the &lt;a href="https://askubuntu.com/q/649027" rel="noopener noreferrer"&gt;window transparent and click-through (question on Ask Ubuntu)&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enjoy(?)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;So what did we learn?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Designing with accessibility and inclusivity in mind helps everyone.&lt;/strong&gt; (Tranit issue)&lt;/li&gt;
&lt;li&gt;"There's more than one way to skin a cat."&lt;/li&gt;
&lt;li&gt;Solving problems is as fun as playing the actual thing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I probably will not implement these solutions/ideas but I might update this post and add some diagrams to better explain them just because.&lt;/p&gt;

&lt;p&gt;Thank you for reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=yepc6o0Bcn8" rel="noopener noreferrer"&gt;Despair Prison's trailer on YouTube&lt;/a&gt; (&lt;strong&gt;WARNING: CONTAINS FLASHING IMAGES!&lt;/strong&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://store.steampowered.com/app/1167790/_/?curator_clanid=36750771" rel="noopener noreferrer"&gt;Despair Prison on Steam&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There's an open source project called Screen Translator (&lt;a href="https://github.com/OneMoreGres/ScreenTranslator" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; and &lt;a href="https://www.softpedia.com/get/Office-tools/Other-Office-Tools/Screen-Translator.shtml" rel="noopener noreferrer"&gt;on Softpedia&lt;/a&gt;) which I haven't tried but seems okay.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>gaming</category>
      <category>visualnovel</category>
      <category>googletranslate</category>
      <category>chinese</category>
    </item>
    <item>
      <title>ZeroMessenger: Improving Facebook Zero's messaging functionality</title>
      <dc:creator>Abdeldjalil Hebal</dc:creator>
      <pubDate>Mon, 06 Apr 2020 19:32:42 +0000</pubDate>
      <link>https://dev.to/djalilhebal/zeromessenger-improving-facebook-zero-s-messaging-functionality-46ek</link>
      <guid>https://dev.to/djalilhebal/zeromessenger-improving-facebook-zero-s-messaging-functionality-46ek</guid>
      <description>&lt;p&gt;&lt;em&gt;Djezzy&lt;/em&gt; is an Algerian mobile network operator. Djezzy provides a zero-rated, text-only version of Facebook: Facebook Zero (0.facebook.com) or 0FB for short.&lt;/p&gt;

&lt;p&gt;Some students (like myself) are practically poor and cannot afford real internet access so they end up relying on this service. What I'm presenting here is my attempt at making Facebook Zero a better shit.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Turning something like this:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjiiseeor5aisyfwjbvbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fjiiseeor5aisyfwjbvbt.png" alt="Facebook Zero screenshot" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;... into something like this:&lt;/em&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2isakdg6s3hyh297bvnz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F2isakdg6s3hyh297bvnz.png" alt="ZeroMessenger screenshot" width="800" height="421"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Disclaimer: This drafty post is a super simplistic explanation of how I wrote an incomplete project that used a discontinued service.&lt;/strong&gt;&lt;br&gt;
&lt;strong&gt;Still, I wanted to publish it since it might be useful to others...&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Idea
&lt;/h2&gt;

&lt;p&gt;After "studying" (i.e. using) Facebook Zero for over a year, I realized that the website is very predictable and has a RESTful-like "structure".&lt;/p&gt;

&lt;p&gt;The idea is simple: &lt;em&gt;If I can manipulate only texts then that's what I'm gonna do&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;We treat Facebook Zero as if it were merely a messy database and an intermediate to exchange data.&lt;br&gt;&lt;br&gt;
So, to send a photo (or any file for that matter), first, convert it to text (&lt;em&gt;base64&lt;/em&gt;) and send it as a text message.&lt;br&gt;&lt;br&gt;
On the other end of the wire, the recipient should convert it back to binary and view it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Gathering data
&lt;/h2&gt;

&lt;p&gt;As I have already intimated, 0FB pages are so predictable that a few &lt;code&gt;document.querySelector&lt;/code&gt; lines allow us to obtain the necessary information to work with.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fg4p50uk4tqnxtq4j2j03.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fg4p50uk4tqnxtq4j2j03.png" alt="0FB, error" width="800" height="449"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Profiles
&lt;/h3&gt;

&lt;p&gt;These are the most important information we need: &lt;code&gt;id&lt;/code&gt;, &lt;code&gt;name&lt;/code&gt;, &lt;code&gt;username&lt;/code&gt;, &lt;code&gt;hasGreenDot&lt;/code&gt; (which signifies the user is active).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;name&lt;/strong&gt; is easily obtained using this simple statement:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#objects_container strong&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt; &lt;span class="c1"&gt;// 'Djalil Dreamski'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;username&lt;/strong&gt;, &lt;strong&gt;phoneNumber&lt;/strong&gt;, and &lt;strong&gt;gender&lt;/strong&gt;...
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Okay, suppose we are on a profile's info page (e.g. https://0.facebook.com/zuck?v=info)&lt;/span&gt;
&lt;span class="c1"&gt;// We can use these two lines to get the user's gender:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rawGender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#root [title="Gender"]`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// 'Gender\nMale'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="c1"&gt;// 'male'&lt;/span&gt;

&lt;span class="c1"&gt;// The above two lines can be used to get other (*useful*) pieces of information, like the `username`&lt;/span&gt;
&lt;span class="c1"&gt;// so let's turn it into a more general and less error-prone function:&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getAttr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;lowerCase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#root [title="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"]`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$container&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerText&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lowerCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;val&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Now we can use it like this:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAttr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Facebook&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// '/zuck'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;phoneNumber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getAttr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Mobile&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// ''&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;id&lt;/strong&gt;
&lt;img alt="0FB profile" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F369gav9ykt78ox7x8tw4.png" width="720" height="1280"&gt;
As far as I know, Facebook assigns an id (&lt;em&gt;FBID&lt;/em&gt;) to each of its objects (profiles, groups, posts, messages, etc.).
In every 'message-able' profile ("page" or "user") webpage, there exists a 'Message' button (a link, actually). We can use this link to get the profile's id.
We can either look for a link whose text content consists of "Message", or whose URL starts with a specific prefix. I chose the latter approach:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Supposing we're on a user's page, and that this user has a 'Message' button/link&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkPrefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://0.facebook.com/messages/thread/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;messageLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linkPrefix&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;messageLink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/thread&lt;/span&gt;&lt;span class="se"&gt;\/(\d&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;// If we were on 0.facebook.com/zuck, 'id' would be '4'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;My id&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpnajq03p1gkafksxv8yv.png" class="article-body-image-wrapper"&gt;&lt;img alt="My profile in Italian" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fpnajq03p1gkafksxv8yv.png" width="415" height="628"&gt;&lt;/a&gt;&lt;br&gt;
We assume I'm already logged in. To get my id, we go to my profile page (&lt;code&gt;/profile.php&lt;/code&gt;) and extract it from the "Registo de atividade" ("Activity Log") link.&lt;br&gt;&lt;br&gt;
We basically repeat the same work we did earlier with &lt;code&gt;id&lt;/code&gt; but this time the link has this pattern: &lt;code&gt;https://0.facebook.com/&amp;lt;MY_ID&amp;gt;/allactivity&lt;/code&gt;.&lt;br&gt;&lt;br&gt;
&lt;strong&gt;Note: Many pieces of code in my app are currently language-specific (only English works for now).&lt;/strong&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;hasGreenDot&lt;/strong&gt; was a little tricky at first as I couldn't just use a simple CSS selector that identifies it:&lt;br&gt;
Apparently some parts of Facebook Zero pages get automatically minified/uglified&lt;br&gt;
so some classes get renamed randomly (e.g. 'cg', 'rt', etc.).&lt;br&gt;&lt;br&gt;
One thing was sure: If the current page contains a &lt;em&gt;greenDot&lt;/em&gt;, there will be a class in the 'style' tag, whose body contains nothing but this rule: &lt;code&gt;{color:#6ba93e;}&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// We could use the above information to do this:&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styleHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hasGreenDot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styleHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;{color:#6ba93e;}&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;// But since we will be using that approach in other places (and for other purposes),&lt;/span&gt;
&lt;span class="c1"&gt;// we actually use a method that retrieves the class's name if it exists.&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getClassName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;styleHtml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;escapedRule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;{}().&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;$&amp;amp;&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// This should do.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rRule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegExp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;.(&lt;/span&gt;&lt;span class="se"&gt;\\&lt;/span&gt;&lt;span class="s1"&gt;w+?)&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;escapedRule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;styleHtml&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rRule&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;className&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// The following may be an empty string or (probably) a two-character name&lt;/span&gt;
&lt;span class="c1"&gt;// const greenDotClassName = getClassName('{color:#6ba93e;}')&lt;/span&gt;
&lt;span class="c1"&gt;// const hasGreenDot = !!greenDotClassName&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Finally, we create a "public" function that gathers all of that information using the above snippets and then returns it.&lt;br&gt;&lt;br&gt;
This function will be attached to the &lt;code&gt;ZeroWorker&lt;/code&gt; namespace (The purpose of this will be shown later).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ZeroWorker is declared globally as `const ZeroWorker = {};`&lt;/span&gt;
&lt;span class="nx"&gt;ZeroWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getProfileInfo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getProfileInfo&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;myId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasGreenDot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;gender&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Conversations
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fra1rnylw5avpgrxyqsd9.png" class="article-body-image-wrapper"&gt;&lt;img alt="0FB Chat, direct, active" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fra1rnylw5avpgrxyqsd9.png" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We can see a chat page's markup like this (at least this is what I recall):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt; &lt;span class="c"&gt;&amp;lt;!-- HEADER --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;strong&amp;gt;&lt;/span&gt;Name!&lt;span class="nt"&gt;&amp;lt;/strong&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;greenDot?&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;statusText (e.g. "Active a few seconds ago")?&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&amp;gt;&lt;/span&gt;Link to chat group info?&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"messageGroup"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"see_older"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;See Older Messages&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;MESSAGE_BLOCK_1&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&amp;gt;&lt;/span&gt;MESSAGE_BLOCK_2&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"see_newer"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;See Newer Messages&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbroygr0jdn2gqyhwlb8p.png" class="article-body-image-wrapper"&gt;&lt;img alt="0FB Chat, group, blocked" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fbroygr0jdn2gqyhwlb8p.png" width="720" height="1280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Each conversation has an id (&lt;em&gt;cid&lt;/em&gt;) AKA thread id (&lt;em&gt;tid&lt;/em&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Group chats contain a 'cid'.&lt;/li&gt;
&lt;li&gt;Individual conversations contain the user's id and my id: &lt;code&gt;cid=ID_X:ID_Y&lt;/code&gt;. My id is either ID_X or ID_Y... Having already obtained my id, the recipient's id is simply not-my-id.&lt;/li&gt;
&lt;li&gt;We can use the individual chat id to get more information about the recipient as shown in the &lt;strong&gt;Profiles&lt;/strong&gt; section, using a link like &lt;code&gt;/profile.php?fbid=&amp;lt;THEIR_ID&amp;gt;&amp;amp;v=info&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;&lt;p&gt;In each conversation, we can use the &lt;code&gt;see_older&lt;/code&gt; and &lt;code&gt;see_newer&lt;/code&gt; links to obtain timestamps of the last and first messages (respectively) in the target section.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Messages can be grouped together in what I call "message blocks"; they are created when a user sends multiple messages consecutively.&lt;/p&gt;&lt;/li&gt;

&lt;li&gt;&lt;p&gt;Each &lt;em&gt;message block&lt;/em&gt; contains 'message ids' (&lt;em&gt;mids&lt;/em&gt;).&lt;br&gt;&lt;br&gt;&lt;br&gt;
Clicking on the &lt;strong&gt;Delete Selected&lt;/strong&gt; link (on the bottom) shows a "Delete" button next to each message. This button is actually a link that contains the message's mids.&lt;/p&gt;&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;These attributes can be used to automatically update the conversation by fetching new messages and deduplicating repeated ones (for technical reasons, duplicates can appear when "scrolling" up and down).&lt;/p&gt;

&lt;p&gt;As with profiles, using the mentioned specifications, we write a function that collects the needed data and returns it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ZeroWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getChat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getChat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hasGreenDot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;statusText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;groupInfoLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seeOlderLink&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seeNewerLink&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;As for sending messages, we use a simple function that performs the actions a user normally does:&lt;br&gt;
Fill in a message and then click send (submit).&lt;/p&gt;

&lt;p&gt;Again, we attach this method to the ZeroWorker namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// This code should be self-explanatory.&lt;/span&gt;
&lt;span class="nx"&gt;ZeroWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sendText&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$form&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#composer_form&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;textarea&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nx"&gt;$input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;str&lt;/span&gt;
  &lt;span class="nx"&gt;$form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Parts
&lt;/h2&gt;

&lt;p&gt;Basically, it consists of three &lt;code&gt;Promise&lt;/code&gt;-based parts: Messenger, Master, and Worker (in addition to "Broker").&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fqz7gb6jutcrgk4daocsm.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fqz7gb6jutcrgk4daocsm.jpeg" alt="ZeroMessenger's complete UML class diagram" width="800" height="431"&gt;&lt;/a&gt;&lt;br&gt;
&lt;code&gt;ZeroMessenger.uml&lt;/code&gt; exported as image using &lt;em&gt;WhiteStarUML&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ZeroWorker
&lt;/h3&gt;

&lt;p&gt;A ZeroWorker (presented in the &lt;strong&gt;Gathering Data&lt;/strong&gt; section) runs on iframes that are opened by &lt;strong&gt;Master&lt;/strong&gt;. (ZeroWorker's scripts get automatically injected in 0FB iframes thanks to Chrome Extension API).&lt;/p&gt;

&lt;p&gt;A Worker listens for orders, executes them, and finally sends a response to Master. &lt;em&gt;ZeroWorkers&lt;/em&gt; and &lt;em&gt;Masters&lt;/em&gt; communicate via &lt;a href="https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage" rel="noopener noreferrer"&gt;cross-document messaging&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The following code shows how jobs are handled.&lt;br&gt;
&lt;strong&gt;This is the reason we have been attaching everything to the ZeroWorker namespace: To dynamically access needed functions.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ZeroWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onOrder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nx"&gt;ZeroWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onOrder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;onOrder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ZeroWorker&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;](...&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="c1"&gt;// Add some useful 'metadata' that Messenger uses to keep its data consistent and up-to-date&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_pageDate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_pageLink&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ZeroMaster
&lt;/h3&gt;

&lt;p&gt;Actually just &lt;strong&gt;Master&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It spawns ZeroWorkers (i.e. iframes), sends orders (&lt;code&gt;job&lt;/code&gt;s) to them, then listens for responses.&lt;/p&gt;

&lt;p&gt;A Master &lt;code&gt;kill()&lt;/code&gt;s the Worker he spawned when they lose their &lt;em&gt;raison d'être&lt;/em&gt; (i.e. the &lt;code&gt;job&lt;/code&gt; is done).&lt;/p&gt;

&lt;p&gt;Also, Master deals with actions that make the page reload (for instance, sending messages) and handles time-outing requests (happens often on shitty cell connections like mine).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dreamski21&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;getProfileInfo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`https://0.facebook.com/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;?v=info`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;master&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;master&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; is &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hasGreenDot&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;online&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;offline&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="c1"&gt;// Probably outputs: "Djalil Dreamski is offline"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As for how it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Master assigns a unique id to each &lt;code&gt;job&lt;/code&gt; object.&lt;/li&gt;
&lt;li&gt;It sends (&lt;code&gt;posts&lt;/code&gt;) the job to the worker and starts listening for a response with that id.&lt;/li&gt;
&lt;li&gt;When a response arrives, the promise is resolved with the response's data object (or rejected if something goes wrong).
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Master&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="nf"&gt;_launch&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;onMessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// Perfect, this is the event we were listening for.&lt;/span&gt;
          &lt;span class="nf"&gt;removeListener&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
          &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Response err&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
            &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;removeListener&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="c1"&gt;// Start listening and then tell ZeroWorker to do the job&lt;/span&gt;
      &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;message&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;onMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;_iframe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contentWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;postMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  ZeroMessenger
&lt;/h3&gt;

&lt;p&gt;ZeroMessenger is the interface that interacts directly with the user.&lt;/p&gt;

&lt;p&gt;Again, it abstracts ZeroMaster and ZeroWorker by providing dedicated classes and methods. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Message&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Sends a text message.
   *
   * @param {string} id - Conversation id
   * @param {string} text
   * @returns {Promise&amp;lt;object&amp;gt;} "Zero Response" from ZeroWorker 
   */&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// url = `https://0.facebook.com/messages/read?fbid=${id}&amp;amp;show_delete_message_button=1&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Conversation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getChatLink&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sendText&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="na"&gt;reloads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Master&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getResponse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And so on, we write classes &lt;code&gt;Profile&lt;/code&gt;, &lt;code&gt;Conversation&lt;/code&gt;, and sub-classes as shown in the UML class diagrams above.&lt;/p&gt;

&lt;p&gt;These classes open different pages/links to do different things. For example, to get a user's info, you open their profile info page then invoke &lt;em&gt;Worker&lt;/em&gt; (by specifying &lt;code&gt;getProfileInfo&lt;/code&gt; as its job) to read and send that info to you.&lt;/p&gt;

&lt;p&gt;Messenger contains all the other parts/classes and eases interaction between them. For instance, to distinguish between my id and another user's, &lt;code&gt;Profile.getTheirId(url)&lt;/code&gt; needs to know my id which is stored in &lt;code&gt;Messenger.moi.id&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;As for dynamically updating the content, ZeroMessenger periodically checks Facebook Zero, the same way a user refreshes pages every few seconds. &lt;em&gt;Really, the goal of this project was to mimic the user's actions + add photos.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;By this point, I have created a good enough API for working with Facebook Zero; the rest is just a basic chat/instant-messaging app.&lt;/p&gt;

&lt;p&gt;Once upon a time when &lt;em&gt;nwjs&lt;/em&gt; used to be called &lt;em&gt;node-webkit&lt;/em&gt; and when &lt;code&gt;Object.observe&lt;/code&gt; was not deprecated, I wrote an &lt;strong&gt;APK Manager&lt;/strong&gt; with a reactive view by observing 'data objects' and updating the DOM when changes occur. It was a fun and interesting project to work on... However, this time I decided to stop reinventing the wheel and use VueJS to handle reactivity, so I can focus on the app's logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  ZeroBroker
&lt;/h3&gt;

&lt;p&gt;This is actually &lt;a href="https://github.com/dreamski21/shit/blob/master/2018-03/ZeroFB++.js" rel="noopener noreferrer"&gt;my original idea&lt;/a&gt;: A "proxy bot" to send and receive binary data using only texts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's inspired by the TCP protocol&lt;/strong&gt; and it works like this:&lt;/p&gt;

&lt;p&gt;The bot logs in using my account and starts watching for incoming messages (including those I send to myself).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Receiving&lt;/strong&gt;: If I receive a file (a photo for example), the bot should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Download it&lt;/li&gt;
&lt;li&gt;Convert it to text and then split it into messages&lt;/li&gt;
&lt;li&gt;Add metadata to those messages&lt;/li&gt;
&lt;li&gt;And finally, send those messages to my inbox.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Sending&lt;/strong&gt;: If I want to send a file to someone:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I simply select something and send it, as in any messaging app.&lt;/li&gt;
&lt;li&gt;ZeroMessenger reads the file and sends its textual representation to my inbox, in addition to some metadata (like whom it is sent to).&lt;/li&gt;
&lt;li&gt;ZeroBroker checks my inbox, collects those pieces, converts them to a binary file, then sends that file to the recipient as if it were sent directly by me.&lt;/li&gt;
&lt;li&gt;The Broker informs me of the progress by sending updates to my inbox. (Feels like talking to myself. Weird.)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;I didn't finish it but I made &lt;em&gt;Zerofy&lt;/em&gt; which lets you do half of the job "manually" (&lt;strong&gt;sending&lt;/strong&gt;),&lt;br&gt;
while the other half is done automatically (&lt;strong&gt;receiving&lt;/strong&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Technicalities and Regrets
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Each message can contain a little more than 2^16 characters, which is approximately 16KB. That requires the image's textual representation to be split into chunks and sent separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Making a simple/limited API for 0FB was tricky since Facebook adds a token named &lt;code&gt;gfid&lt;/code&gt; (whose value is randomly generated) to some links and forms (probably in order to fight CSRF attacks). This means that some pages need to be opened in order to get the value of &lt;code&gt;gfid&lt;/code&gt; before actually doing the desired actions: Sending and deleting messages, and changing my Active Status.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Facebook's HTTP response contains a header that tells the browser to not allow iframes. We simply intercept the response and remove this troublesome header.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Data is half processed by &lt;em&gt;Worker&lt;/em&gt; and the rest is handled by &lt;em&gt;Messenger/Master&lt;/em&gt;.&lt;br&gt;
That's confusing. Only one of them should take the responsibility and do most of the work (preferably Messenger while Workers only gather raw data and "obey orders").&lt;/p&gt;




&lt;p&gt;The way ZeroMessenger works is similar to crawlers (which Facebook tries to prevent), this necessitates that we mimic a browser. I could use libraries to grab pages (using &lt;code&gt;axios&lt;/code&gt;), parse them and extract relevant info (using &lt;code&gt;cheerio&lt;/code&gt;), compose requests and send them. This solution would be independent of the browser and work on Node, the possibilities would be limitless...&lt;/p&gt;

&lt;p&gt;That wasn't what I did. Wanting to keep it simple and having used Google Chrome's Extension API before, I decided to use iframes and inject scripts into them. This is a bad approach since it's costly (needlessly rendering pages and loading images) and gives less control (like catching network errors and redirects and stuff).&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;And there you have it, Facebook Zero is a better shit.&lt;br&gt;
You can check the source code &lt;a href="https://github.com/dreamski21/zero" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt;... and why not fork it and finish it...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;JavaScript is amazing: It has simple yet powerful APIs that can be used to make complex projects.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;VueJS is beautiful: Simple syntax and, as its website promotes it, "incrementally adoptable".&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Nothing compares to learning by doing: In reality, this was a huge experimental project: VueJS, Promises and async/await, postMessage API, "parallelism", etc.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Thanks
&lt;/h2&gt;

&lt;p&gt;I'd like to thank my friend Wanis R. for the help he provided (beta reading, beta testing, allowing me to use his real internet sometimes, etc.) and for his continuous support and encouragement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2018-11 Update&lt;/strong&gt;: Djezzy's Facebook Zero and Wikipedia no longer work. Sad.&lt;/p&gt;

</description>
      <category>facebook</category>
      <category>javascript</category>
      <category>vue</category>
      <category>reverseengineering</category>
    </item>
  </channel>
</rss>
