{"$schema":"https://ejfox.com/schema/page-twin@1.json","kind":"blog_post","url":"https://ejfox.com/blog/2025/paramilitary-telegram-leaks-001","json_url":"https://ejfox.com/blog/2025/paramilitary-telegram-leaks-001.json","generator":"ejfox.com/json-twin@1","data":{"cacheVersion":"2026-05-13-gear-cards","html":"<h2 class=\"\" id=\"paramilitary-telegram-leaks-from-dump-to-data\">Paramilitary Telegram Leaks: From Dump To Data</h2>\n<p class=\"\">The other day I was perusing Mastodon, as one does, and noticed a post from <a href=\"https://theintercept.com/staff/micah-lee/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://theintercept.com/staff/micah-lee/\">journalist</a> and <a href=\"https://hacksandleaks.com\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://hacksandleaks.com\">author</a> Micah Lee <a href=\"https://micahflee.com/exploring-the-paramilitary-leaks/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://micahflee.com/exploring-the-paramilitary-leaks/\">saying he was exploring</a> a 200GB dump from <a href=\"https://www.propublica.org/article/ap3-oath-keepers-militia-mole\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://www.propublica.org/article/ap3-oath-keepers-militia-mole\">American paramilitary groups.</a></p>\n<p class=\"\">This leak was made possible through the incredible bravery of <a href=\"https://www.reddit.com/user/shitshowshaman/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://www.reddit.com/user/shitshowshaman/\">John Williams<svg class=\"inline-block w-4 h-4 ml-1 opacity-50 dark:opacity-75 group-hover:opacity-100 transition-opacity align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 512 512\"><path fill=\"currentColor\" d=\"M0 256C0 114.6 114.6 0 256 0s256 114.6 256 256s-114.6 256-256 256H37.1c-13.7 0-20.5-16.5-10.9-26.2L75 437C28.7 390.7 0 326.7 0 256m349.6-102.4c23.6 0 42.7-19.1 42.7-42.7s-19.1-42.7-42.7-42.7c-20.6 0-37.8 14.6-41.8 34c-34.5 3.7-61.4 33-61.4 68.4v.2c-37.5 1.6-71.8 12.3-99 29.1c-10.1-7.8-22.8-12.5-36.5-12.5c-33 0-59.8 26.8-59.8 59.8c0 24 14.1 44.6 34.4 54.1c2 69.4 77.6 125.2 170.6 125.2s168.7-55.9 170.6-125.3c20.2-9.6 34.1-30.2 34.1-54c0-33-26.8-59.8-59.8-59.8c-13.7 0-26.3 4.6-36.4 12.4c-27.4-17-62.1-27.7-100-29.1v-.2c0-25.4 18.9-46.5 43.4-49.9c4.4 18.8 21.3 32.8 41.5 32.8zm-172.5 93.3c16.7 0 29.5 17.6 28.5 39.3s-13.5 29.6-30.3 29.6s-31.4-8.8-30.4-30.5S160.3 247 177 247zm190.1 38.3c1 21.7-13.7 30.5-30.4 30.5s-29.3-7.9-30.3-29.6s11.8-39.3 28.5-39.3s31.2 16.6 32.1 38.3zm-48.1 56.7c-10.3 24.6-34.6 41.9-63 41.9s-52.7-17.3-63-41.9c-1.2-2.9.8-6.2 3.9-6.5c18.4-1.9 38.3-2.9 59.1-2.9s40.7 1 59.1 2.9c3.1.3 5.1 3.6 3.9 6.5\" class=\"\"></path></svg></a> who infiltrated these groups and exfiltrated all this data. He is currently <a href=\"https://www.every.org/psstorg-inc/f/help-john-infiltrate\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://www.every.org/psstorg-inc/f/help-john-infiltrate\">crowdfunding $10k</a> in part, to \"move safe houses and to change his identity\" and he also <a href=\"https://www.patreon.com/mobiusblack\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://www.patreon.com/mobiusblack\">has a Patreon where you can support him<svg class=\"inline-block w-4 h-4 ml-1 opacity-50 dark:opacity-75 group-hover:opacity-100 transition-opacity align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M22.957 7.21c-.004-3.064-2.391-5.576-5.191-6.482c-3.478-1.125-8.064-.962-11.384.604C2.357 3.231 1.093 7.391 1.046 11.54c-.039 3.411.302 12.396 5.369 12.46c3.765.047 4.326-4.804 6.068-7.141c1.24-1.662 2.836-2.132 4.801-2.618c3.376-.836 5.678-3.501 5.673-7.031\" class=\"\"></path></svg></a></p>\n<figure role=\"figure\" aria-label=\"Terminal showing system processes and a shell script that fetches tasks, recent repos, Mastodon posts, and calculates sunset time for New York City.\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741194693/Screenshot_2025-03-05_at_12.11.19_PM.png\" alt=\"Terminal showing system processes and a shell script that fetches tasks, recent repos, Mastodon posts, and calculates sunset time for New York City.\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/v1741194693/Screenshot_2025-03-05_at_12.11.19_PM.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/v1741194693/Screenshot_2025-03-05_at_12.11.19_PM.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741194693/Screenshot_2025-03-05_at_12.11.19_PM.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/v1741194693/Screenshot_2025-03-05_at_12.11.19_PM.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Terminal showing system processes and a shell script that fetches tasks, recent repos, Mastodon posts, and calculates sunset time for New York City.\" width=\"3692\" height=\"2354\" style=\"--splay-rot:2.03deg;\" data-dimensions=\"3692×2354\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Terminal showing system processes and a shell script that fetches tasks, recent repos, Mastodon posts, and calculates sunset time for New York City.</figcaption>\n</figure>\n<p class=\"\">I hadn't heard of such a dump before, and immediately sent Micah an email offering to help with any dataviz.</p>\n<figure role=\"figure\" aria-label=\"Email from journalist EJ Fox proposing interactive graphics collaboration to visualize a large paramilitary dataset using network analysis techniques.\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741542940/Screenshot_2025-03-09_at_1.55.27_PM.png\" alt=\"Email from journalist EJ Fox proposing interactive graphics collaboration to visualize a large paramilitary dataset using network analysis techniques.\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/v1741542940/Screenshot_2025-03-09_at_1.55.27_PM.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/v1741542940/Screenshot_2025-03-09_at_1.55.27_PM.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741542940/Screenshot_2025-03-09_at_1.55.27_PM.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/v1741542940/Screenshot_2025-03-09_at_1.55.27_PM.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Email from journalist EJ Fox proposing interactive graphics collaboration to visualize a large paramilitary dataset using network analysis techniques.\" width=\"1618\" height=\"1494\" style=\"--splay-rot:-0.65deg;\" data-dimensions=\"1618×1494\" class=\"img-full my-8 rounded-sm img-splay aspect-square w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Email from journalist EJ Fox proposing interactive graphics collaboration to visualize a large paramilitary dataset using network analysis techniques.</figcaption>\n</figure>\n<h2 class=\"\" id=\"from-sqlite-to-parquet-processing-telegram-leaks-for-fast-web-visualization\">From SQLite to Parquet: Processing Telegram Leaks for Fast Web Visualization</h2>\n<h3 class=\"\" id=\"the-challenge\">The Challenge</h3>\n<ol class=\"list-decimal\">\n<li class=\"\">Extract the relevant data</li>\n<li class=\"\">Convert it to a web-friendly format</li>\n<li class=\"\">Make it quickly accessible for visualization</li>\n<li class=\"\">Cache it for fast loading across browsers</li>\n</ol>\n<figure role=\"figure\" aria-label=\"Torrent client downloading conspiracy-related files including election terrorism claims, state leaders chats, and constitutional IRS documents.\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741236542/Screenshot_2025-03-05_at_11.48.45_PM.png\" alt=\"Torrent client downloading conspiracy-related files including election terrorism claims, state leaders chats, and constitutional IRS documents.\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/v1741236542/Screenshot_2025-03-05_at_11.48.45_PM.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/v1741236542/Screenshot_2025-03-05_at_11.48.45_PM.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741236542/Screenshot_2025-03-05_at_11.48.45_PM.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/v1741236542/Screenshot_2025-03-05_at_11.48.45_PM.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Torrent client downloading conspiracy-related files including election terrorism claims, state leaders chats, and constitutional IRS documents.\" width=\"3600\" height=\"2338\" style=\"--splay-rot:-1.43deg;\" data-dimensions=\"3600×2338\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Torrent client downloading conspiracy-related files including election terrorism claims, state leaders chats, and constitutional IRS documents.</figcaption>\n</figure>\n<h3 class=\"\" id=\"the-workflow\">The Workflow</h3>\n<p class=\"\">We start with the <a href=\"https://github.com/micahflee/paramilitary-leaks\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://github.com/micahflee/paramilitary-leaks\">repo Micah started<svg class=\"inline-block w-4 h-4 ml-1 opacity-50 dark:opacity-75 group-hover:opacity-100 transition-opacity align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" class=\"\"></path></svg></a> that takes the raw dump of .html exports from Telegram and transforms them into a structured sqlite db we can work with.</p>\n<p class=\"\">Once you get that repo setup you can just do</p>\n<figure data-rehype-pretty-code-figure=\"\" class=\"\"><pre class=\"md-pre\" style=\"background-color:#09090b;color:#cccac2\" tabindex=\"0\" data-language=\"bash\" data-theme=\"\"><code data-language=\"bash\" data-theme=\"\" style=\"display: grid;\" class=\"md-codeblock\"><span class=\"\" data-line=\"\"><span style=\"color:#73D0FF\" class=\"\">poetry</span><span style=\"color:#D5FF80\" class=\"\"> run</span><span style=\"color:#D5FF80\" class=\"\"> tasks</span><span style=\"color:#D5FF80\" class=\"\"> build-db</span><span style=\"color:#D5FF80\" class=\"\"> ~/RAW-LEAK-LOCATION`</span></span><button type=\"button\" title=\"Copy code\" aria-label=\"Copy code\" data=\"poetry run tasks build-db ~/RAW-LEAK-LOCATION&#x60;\" class=\"rehype-pretty-copy\" onclick=\"navigator.clipboard.writeText(this.attributes.data.value);this.classList.add(&#x27;rehype-pretty-copied&#x27;);window.setTimeout(() => this.classList.remove(&#x27;rehype-pretty-copied&#x27;), 3000);\"><span class=\"ready\"></span><span class=\"success\"></span></button><style class=\"\">:root {--copy-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23adadad' d='M16.187 9.5H12.25a1.75 1.75 0 0 0-1.75 1.75v28.5c0 .967.784 1.75 1.75 1.75h23.5a1.75 1.75 0 0 0 1.75-1.75v-28.5a1.75 1.75 0 0 0-1.75-1.75h-3.937a4.25 4.25 0 0 1-4.063 3h-7.5a4.25 4.25 0 0 1-4.063-3M31.813 7h3.937A4.25 4.25 0 0 1 40 11.25v28.5A4.25 4.25 0 0 1 35.75 44h-23.5A4.25 4.25 0 0 1 8 39.75v-28.5A4.25 4.25 0 0 1 12.25 7h3.937a4.25 4.25 0 0 1 4.063-3h7.5a4.25 4.25 0 0 1 4.063 3M18.5 8.25c0 .966.784 1.75 1.75 1.75h7.5a1.75 1.75 0 1 0 0-3.5h-7.5a1.75 1.75 0 0 0-1.75 1.75'/%3E%3C/svg%3E\");--success-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2366ff85' d='M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z'/%3E%3C/svg%3E\");}pre:has(code) {position: relative;}pre button.rehype-pretty-copy {right: 1px;padding: 0;width: 24px;height: 24px;display: flex;margin-top: 2px;margin-right: 8px;position: absolute;border-radius: 25%;backdrop-filter: blur(3px);& span {width: 100%;aspect-ratio: 1 / 1;}& .ready {background-image: var(--copy-icon);}& .success {display: none; background-image: var(--success-icon);}}&.rehype-pretty-copied {& .success {display: block;} & .ready {display: none;}}pre button.rehype-pretty-copy.rehype-pretty-copied {opacity: 1;& .ready { display: none; }& .success { display: block; }}</style></code></pre></figure>\n<p class=\"\">… and you get a sqlite database in your <code class=\"md-inline-code\">output</code> folder.</p>\n<h4 class=\"\" id=\"step-1-export-from-sqlite-to-csv\">Step 1: Export from SQLite to CSV</h4>\n<p class=\"\">First, I extract the messages table to a .csv using SQLite's command-line tool:</p>\n<figure data-rehype-pretty-code-figure=\"\" class=\"\"><pre class=\"md-pre\" style=\"background-color:#09090b;color:#cccac2\" tabindex=\"0\" data-language=\"bash\" data-theme=\"\"><code data-language=\"bash\" data-theme=\"\" style=\"display: grid;\" class=\"md-codeblock\"><span class=\"\" data-line=\"\"><span style=\"color:#73D0FF\" class=\"\">sqlite3</span><span style=\"color:#95E6CB\" class=\"\"> -header</span><span style=\"color:#95E6CB\" class=\"\"> -csv</span><span style=\"color:#D5FF80\" class=\"\"> output/data.db</span><span style=\"color:#D5FF80\" class=\"\"> 'select * from messages;'</span><span style=\"color:#F29E74\" class=\"\"> ></span><span style=\"color:#D5FF80\" class=\"\"> output/telegram_chats.csv</span></span><button type=\"button\" title=\"Copy code\" aria-label=\"Copy code\" data=\"sqlite3 -header -csv output/data.db &#x27;select * from messages;&#x27; > output/telegram_chats.csv\" class=\"rehype-pretty-copy\" onclick=\"navigator.clipboard.writeText(this.attributes.data.value);this.classList.add(&#x27;rehype-pretty-copied&#x27;);window.setTimeout(() => this.classList.remove(&#x27;rehype-pretty-copied&#x27;), 3000);\"><span class=\"ready\"></span><span class=\"success\"></span></button><style class=\"\">:root {--copy-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23adadad' d='M16.187 9.5H12.25a1.75 1.75 0 0 0-1.75 1.75v28.5c0 .967.784 1.75 1.75 1.75h23.5a1.75 1.75 0 0 0 1.75-1.75v-28.5a1.75 1.75 0 0 0-1.75-1.75h-3.937a4.25 4.25 0 0 1-4.063 3h-7.5a4.25 4.25 0 0 1-4.063-3M31.813 7h3.937A4.25 4.25 0 0 1 40 11.25v28.5A4.25 4.25 0 0 1 35.75 44h-23.5A4.25 4.25 0 0 1 8 39.75v-28.5A4.25 4.25 0 0 1 12.25 7h3.937a4.25 4.25 0 0 1 4.063-3h7.5a4.25 4.25 0 0 1 4.063 3M18.5 8.25c0 .966.784 1.75 1.75 1.75h7.5a1.75 1.75 0 1 0 0-3.5h-7.5a1.75 1.75 0 0 0-1.75 1.75'/%3E%3C/svg%3E\");--success-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2366ff85' d='M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z'/%3E%3C/svg%3E\");}pre:has(code) {position: relative;}pre button.rehype-pretty-copy {right: 1px;padding: 0;width: 24px;height: 24px;display: flex;margin-top: 2px;margin-right: 8px;position: absolute;border-radius: 25%;backdrop-filter: blur(3px);& span {width: 100%;aspect-ratio: 1 / 1;}& .ready {background-image: var(--copy-icon);}& .success {display: none; background-image: var(--success-icon);}}&.rehype-pretty-copied {& .success {display: block;} & .ready {display: none;}}pre button.rehype-pretty-copy.rehype-pretty-copied {opacity: 1;& .ready { display: none; }& .success { display: block; }}</style></code></pre></figure>\n<p class=\"\">This gives us a clean CSV file with all our message data, ready for the next transformation- we could also play around with something like <a href=\"https://github.com/wireservice/csvkit\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://github.com/wireservice/csvkit\">csvkit<svg class=\"inline-block w-4 h-4 ml-1 opacity-50 dark:opacity-75 group-hover:opacity-100 transition-opacity align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" class=\"\"></path></svg></a> or <a href=\"https://www.visidata.org\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://www.visidata.org\">visitdata</a></p>\n<h4 class=\"\" id=\"step-2-convert-csv-to-parquet\">Step 2: Convert CSV to Parquet</h4>\n<p class=\"\">Parquet is a columnar storage format that's incredibly efficient for analytical queries and works brilliantly in browser environments. I use DuckDB (an in-process analytical database) to handle the conversion:</p>\n<figure data-rehype-pretty-code-figure=\"\" class=\"\"><pre class=\"md-pre\" style=\"background-color:#09090b;color:#cccac2\" tabindex=\"0\" data-language=\"bash\" data-theme=\"\"><code data-language=\"bash\" data-theme=\"\" style=\"display: grid;\" class=\"md-codeblock\"><span class=\"\" data-line=\"\"><span style=\"color:#73D0FF\" class=\"\">duckdb</span><span style=\"color:#95E6CB\" class=\"\"> -c</span><span style=\"color:#D5FF80\" class=\"\"> \"INSTALL parquet; LOAD parquet; CREATE TABLE temp AS SELECT * FROM read_csv('output/telegram_chats.csv'); COPY temp TO 'output/telegram_chats.r5.parquet' (FORMAT PARQUET, COMPRESSION 'SNAPPY');\"</span></span><button type=\"button\" title=\"Copy code\" aria-label=\"Copy code\" data=\"duckdb -c &#x22;INSTALL parquet; LOAD parquet; CREATE TABLE temp AS SELECT * FROM read_csv(&#x27;output/telegram_chats.csv&#x27;); COPY temp TO &#x27;output/telegram_chats.r5.parquet&#x27; (FORMAT PARQUET, COMPRESSION &#x27;SNAPPY&#x27;);&#x22;\" class=\"rehype-pretty-copy\" onclick=\"navigator.clipboard.writeText(this.attributes.data.value);this.classList.add(&#x27;rehype-pretty-copied&#x27;);window.setTimeout(() => this.classList.remove(&#x27;rehype-pretty-copied&#x27;), 3000);\"><span class=\"ready\"></span><span class=\"success\"></span></button><style class=\"\">:root {--copy-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23adadad' d='M16.187 9.5H12.25a1.75 1.75 0 0 0-1.75 1.75v28.5c0 .967.784 1.75 1.75 1.75h23.5a1.75 1.75 0 0 0 1.75-1.75v-28.5a1.75 1.75 0 0 0-1.75-1.75h-3.937a4.25 4.25 0 0 1-4.063 3h-7.5a4.25 4.25 0 0 1-4.063-3M31.813 7h3.937A4.25 4.25 0 0 1 40 11.25v28.5A4.25 4.25 0 0 1 35.75 44h-23.5A4.25 4.25 0 0 1 8 39.75v-28.5A4.25 4.25 0 0 1 12.25 7h3.937a4.25 4.25 0 0 1 4.063-3h7.5a4.25 4.25 0 0 1 4.063 3M18.5 8.25c0 .966.784 1.75 1.75 1.75h7.5a1.75 1.75 0 1 0 0-3.5h-7.5a1.75 1.75 0 0 0-1.75 1.75'/%3E%3C/svg%3E\");--success-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2366ff85' d='M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z'/%3E%3C/svg%3E\");}pre:has(code) {position: relative;}pre button.rehype-pretty-copy {right: 1px;padding: 0;width: 24px;height: 24px;display: flex;margin-top: 2px;margin-right: 8px;position: absolute;border-radius: 25%;backdrop-filter: blur(3px);& span {width: 100%;aspect-ratio: 1 / 1;}& .ready {background-image: var(--copy-icon);}& .success {display: none; background-image: var(--success-icon);}}&.rehype-pretty-copied {& .success {display: block;} & .ready {display: none;}}pre button.rehype-pretty-copy.rehype-pretty-copied {opacity: 1;& .ready { display: none; }& .success { display: block; }}</style></code></pre></figure>\n<p class=\"\">The compression with Snappy ensures it's both compact and fast to decompress.</p>\n<h4 class=\"\" id=\"step-3-host-on-r2-with-cloudflare-caching\">Step 3: Host on R2 with Cloudflare Caching</h4>\n<p class=\"\">To make the data available globally with minimal latency (and cost to me) we throw it on Cloudflare R2 for hosting</p>\n<pre class=\"md-pre\"><code class=\"md-inline-code\"># R2 Configuration\nR2_PARQUET_URL=https://r2.ejfox.com/para-leaks/telegram_chats.r5.parquet\n</code></pre>\n<h3 class=\"\" id=\"the-frontend\">The Frontend</h3>\n<p class=\"\">I've developed a Nuxt / <a href=\"https://github.com/flekschas/regl-scatterplot\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://github.com/flekschas/regl-scatterplot\">regl-scatterplot<svg class=\"inline-block w-4 h-4 ml-1 opacity-50 dark:opacity-75 group-hover:opacity-100 transition-opacity align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" class=\"\"></path></svg></a> frontend application to interact with this data, available on GitHub:</p>\n<p class=\"\"><a href=\"https://github.com/ejfox/paramilitary-leaks-frontend\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://github.com/ejfox/paramilitary-leaks-frontend\">https://github.com/ejfox/paramilitary-leaks-frontend<svg class=\"inline-block w-4 h-4 ml-1 opacity-50 dark:opacity-75 group-hover:opacity-100 transition-opacity align-text-bottom\" xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M12 .297c-6.63 0-12 5.373-12 12c0 5.303 3.438 9.8 8.205 11.385c.6.113.82-.258.82-.577c0-.285-.01-1.04-.015-2.04c-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729c1.205.084 1.838 1.236 1.838 1.236c1.07 1.835 2.809 1.305 3.495.998c.108-.776.417-1.305.76-1.605c-2.665-.3-5.466-1.332-5.466-5.93c0-1.31.465-2.38 1.235-3.22c-.135-.303-.54-1.523.105-3.176c0 0 1.005-.322 3.3 1.23c.96-.267 1.98-.399 3-.405c1.02.006 2.04.138 3 .405c2.28-1.552 3.285-1.23 3.285-1.23c.645 1.653.24 2.873.12 3.176c.765.84 1.23 1.91 1.23 3.22c0 4.61-2.805 5.625-5.475 5.92c.42.36.81 1.096.81 2.22c0 1.606-.015 2.896-.015 3.286c0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12\" class=\"\"></path></svg></a></p>\n<div class=\"video-container relative aspect-video w-full max-w-full mx-auto rounded-lg overflow-hidden shadow-lg\">\n  <video controls preload=\"none\" poster=\"https://res.cloudinary.com/ejf/video/upload/v1741568702/text-search.jpg\" class=\"w-full h-full object-cover\">\n    <source src=\"https://res.cloudinary.com/ejf/video/upload/v1741568702/text-search.mp4\" type=\"video/mp4\" class=\"\">\n    Your browser does not support the video tag.\n  </video>\n</div>\n<p class=\"\">You can view it the front-end <a href=\"https://para-leaks.ejfox.com\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://para-leaks.ejfox.com\">https://para-leaks.ejfox.com</a></p>\n<h3 class=\"\" id=\"data-exploration-examples\">Data Exploration Examples</h3>\n<p class=\"\">Now that we've chopped and screwed our data between sqlite, parquet, and .csv – depending on what application we want to use it for, we can start asking interesting questions from the sqlite database with <a href=\"https://datasette.io\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://datasette.io\">datasette</a>.</p>\n<p class=\"\">For example, to find the top senders of media you could run:</p>\n<figure data-rehype-pretty-code-figure=\"\" class=\"\"><pre class=\"md-pre\" style=\"background-color:#09090b;color:#cccac2\" tabindex=\"0\" data-language=\"sql\" data-theme=\"\"><code data-language=\"sql\" data-theme=\"\" style=\"display: grid;\" class=\"md-codeblock\"><span class=\"\" data-line=\"\"><span style=\"color:#FFAD66\" class=\"\">select</span><span style=\"color:#CCCAC2\" class=\"\"> id, </span><span style=\"color:#FFAD66\" class=\"\">timestamp</span><span style=\"color:#CCCAC2\" class=\"\">, sender, </span><span style=\"color:#FFAD66\" class=\"\">text</span><span style=\"color:#CCCAC2\" class=\"\">, media_note, media_filename, </span><span style=\"color:#FFAD66\" class=\"\">filename</span><span style=\"color:#CCCAC2\" class=\"\">, group_chat_id </span></span>\n<span class=\"\" data-line=\"\"><span style=\"color:#FFAD66\" class=\"\">from</span><span style=\"color:#CCCAC2\" class=\"\"> messages </span></span>\n<span class=\"\" data-line=\"\"><span style=\"color:#FFAD66\" class=\"\">where</span><span style=\"color:#D5FF80\" class=\"\"> \"media_filename\"</span><span style=\"color:#FFAD66\" class=\"\"> is not null</span><span style=\"color:#CCCAC2\" class=\"\"> </span></span>\n<span class=\"\" data-line=\"\"><span style=\"color:#FFAD66\" class=\"\">order by</span><span style=\"color:#CCCAC2\" class=\"\"> media_filename </span><span style=\"color:#FFAD66\" class=\"\">desc</span></span>\n<span class=\"\" data-line=\"\"><span style=\"color:#FFAD66\" class=\"\">limit</span><span style=\"color:#DFBFFF\" class=\"\"> 101</span></span><button type=\"button\" title=\"Copy code\" aria-label=\"Copy code\" data=\"select id, timestamp, sender, text, media_note, media_filename, filename, group_chat_id \nfrom messages \nwhere &#x22;media_filename&#x22; is not null \norder by media_filename desc\nlimit 101\" class=\"rehype-pretty-copy\" onclick=\"navigator.clipboard.writeText(this.attributes.data.value);this.classList.add(&#x27;rehype-pretty-copied&#x27;);window.setTimeout(() => this.classList.remove(&#x27;rehype-pretty-copied&#x27;), 3000);\"><span class=\"ready\"></span><span class=\"success\"></span></button><style class=\"\">:root {--copy-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 48 48'%3E%3Cpath fill='%23adadad' d='M16.187 9.5H12.25a1.75 1.75 0 0 0-1.75 1.75v28.5c0 .967.784 1.75 1.75 1.75h23.5a1.75 1.75 0 0 0 1.75-1.75v-28.5a1.75 1.75 0 0 0-1.75-1.75h-3.937a4.25 4.25 0 0 1-4.063 3h-7.5a4.25 4.25 0 0 1-4.063-3M31.813 7h3.937A4.25 4.25 0 0 1 40 11.25v28.5A4.25 4.25 0 0 1 35.75 44h-23.5A4.25 4.25 0 0 1 8 39.75v-28.5A4.25 4.25 0 0 1 12.25 7h3.937a4.25 4.25 0 0 1 4.063-3h7.5a4.25 4.25 0 0 1 4.063 3M18.5 8.25c0 .966.784 1.75 1.75 1.75h7.5a1.75 1.75 0 1 0 0-3.5h-7.5a1.75 1.75 0 0 0-1.75 1.75'/%3E%3C/svg%3E\");--success-icon: url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2366ff85' d='M9 16.17L5.53 12.7a.996.996 0 1 0-1.41 1.41l4.18 4.18c.39.39 1.02.39 1.41 0L20.29 7.71a.996.996 0 1 0-1.41-1.41z'/%3E%3C/svg%3E\");}pre:has(code) {position: relative;}pre button.rehype-pretty-copy {right: 1px;padding: 0;width: 24px;height: 24px;display: flex;margin-top: 2px;margin-right: 8px;position: absolute;border-radius: 25%;backdrop-filter: blur(3px);& span {width: 100%;aspect-ratio: 1 / 1;}& .ready {background-image: var(--copy-icon);}& .success {display: none; background-image: var(--success-icon);}}&.rehype-pretty-copied {& .success {display: block;} & .ready {display: none;}}pre button.rehype-pretty-copy.rehype-pretty-copied {opacity: 1;& .ready { display: none; }& .success { display: block; }}</style></code></pre></figure>\n<figure role=\"figure\" aria-label=\"Datasette query for top media senders\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741547576/Screenshot_2025-03-09_at_3.12.44_PM.png\" alt=\"Datasette query for top media senders\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/v1741547576/Screenshot_2025-03-09_at_3.12.44_PM.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/v1741547576/Screenshot_2025-03-09_at_3.12.44_PM.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741547576/Screenshot_2025-03-09_at_3.12.44_PM.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/v1741547576/Screenshot_2025-03-09_at_3.12.44_PM.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Datasette query for top media senders\" width=\"1630\" height=\"1984\" style=\"--splay-rot:0.9deg;\" data-dimensions=\"1630×1984\" class=\"img-full my-8 rounded-sm img-splay aspect-[3/4] w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Datasette query for top media senders</figcaption>\n</figure>\n<p class=\"\">Because so much information is in these media files, I took a stab at having Gemini write data transcriptions of each photo so they could be indexed for text search later.</p>\n<figure role=\"figure\" aria-label=\"Man in Santa Claus costume making peace sign in a lobby, wearing traditional red suit with white fur trim and black belt.\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741377995/Screenshot_2025-03-07_at_3.06.14_PM.png\" alt=\"Man in Santa Claus costume making peace sign in a lobby, wearing traditional red suit with white fur trim and black belt.\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/v1741377995/Screenshot_2025-03-07_at_3.06.14_PM.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/v1741377995/Screenshot_2025-03-07_at_3.06.14_PM.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741377995/Screenshot_2025-03-07_at_3.06.14_PM.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/v1741377995/Screenshot_2025-03-07_at_3.06.14_PM.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"Man in Santa Claus costume making peace sign in a lobby, wearing traditional red suit with white fur trim and black belt.\" width=\"3600\" height=\"2338\" style=\"--splay-rot:2.62deg;\" data-dimensions=\"3600×2338\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">Man in Santa Claus costume making peace sign in a lobby, wearing traditional red suit with white fur trim and black belt.</figcaption>\n</figure>\n<figure role=\"figure\" aria-label=\"CSV spreadsheet cataloging visual media assets with detailed descriptions of photographs depicting outdoor scenes, people, animals, and landscapes.\" class=\"\">\n<img src=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741378110/Screenshot_2025-03-07_at_3.08.18_PM.png\" alt=\"CSV spreadsheet cataloging visual media assets with detailed descriptions of photographs depicting outdoor scenes, people, animals, and landscapes.\" loading=\"lazy\" decoding=\"async\" srcset=\"https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_640/v1741378110/Screenshot_2025-03-07_at_3.08.18_PM.png 640w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_960/v1741378110/Screenshot_2025-03-07_at_3.08.18_PM.png 960w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1280/v1741378110/Screenshot_2025-03-07_at_3.08.18_PM.png 1280w, https://res.cloudinary.com/ejf/image/upload/c_scale,f_auto,q_auto:good,w_1920/v1741378110/Screenshot_2025-03-07_at_3.08.18_PM.png 1920w\" sizes=\"(max-width: 430px) 92vw, (max-width: 768px) 85vw, 900px\" title=\"CSV spreadsheet cataloging visual media assets with detailed descriptions of photographs depicting outdoor scenes, people, animals, and landscapes.\" width=\"3620\" height=\"1086\" style=\"--splay-rot:0.6deg;\" data-dimensions=\"3620×1086\" class=\"img-full my-8 rounded-sm img-splay w-full mx-auto py-4\" crossorigin=\"anonymous\">\n<figcaption class=\"\">CSV spreadsheet cataloging visual media assets with detailed descriptions of photographs depicting outdoor scenes, people, animals, and landscapes.</figcaption>\n</figure>\n<p class=\"\">This experiment went pretty well, but I shelved it to come back to, there's enough to analyze here without involving LLMs and prompt engineering.</p>\n<h3 class=\"\" id=\"building-custom-visualizations\">Building Custom Visualizations</h3>\n<p class=\"\">The real power of this approach is that we can build all sorts of different front-ends or visualizations on top of this data. The Parquet format is super-fast in the browser, making it ideal for interactive visualizations. We've taken a 200GB leak and turned it into something we can explore in the browser.</p>\n<p class=\"\">Some ideas I'm exploring:</p>\n<ul class=\"list-disc\">\n<li class=\"\">Timeline visualizations of message frequency</li>\n<li class=\"\">Network graphs of user interactions</li>\n<li class=\"\">Text analysis and keyword extraction</li>\n<li class=\"\">Sentiment analysis over time</li>\n</ul>\n<svg class=\"mx-auto my-8 w-full max-w-prose\" height=\"1\">\n  <line x1=\"0\" y1=\"0\" x2=\"100%\" y2=\"0\" stroke=\"currentColor\" stroke-width=\"1\" stroke-dasharray=\"2,4\" />\n</svg>\n<p class=\"\">For more information on this topic, I recommend reading Micah Lee's excellent article:</p>\n<p class=\"\"><a href=\"https://micahflee.com/exploring-the-paramilitary-leaks/\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://micahflee.com/exploring-the-paramilitary-leaks/\">Exploring the Paramilitary Leaks</a></p>\n<p class=\"\">ProPublica: <a href=\"https://www.propublica.org/article/ap3-oath-keepers-militia-mole\" target=\"_blank\" rel=\"noopener noreferrer\" class=\"external-link group inline-flex items-center text-blue-600 dark:text-blue-400\" data-preview-url=\"https://www.propublica.org/article/ap3-oath-keepers-militia-mole\">The Militia and the Mole (Jan. 4, 2025)</a></p>","title":"Processing Telegram Leaks for Fast Web Visualization","metadata":{"date":"2025-03-09T17:49:01.000Z","modified":"2025-03-16T01:25:05.000Z","title":"Processing Telegram Leaks for Fast Web Visualization","dek":"In which I describe my workflow for transforming a Telegram database dump into a web-friendly format for analysis and visualization","inprogress":true,"tags":["data","dataviz","journalism","politics"],"words":782,"images":6,"imageDetails":{"total":6,"cloudinary":6,"withDimensions":0},"links":21,"codeBlocks":5,"headers":{"h2":2},"toc":[{"text":"Paramilitary Telegram Leaks: From Dump To Data","slug":"paramilitary-telegram-leaks-from-dump-to-data","level":"h2","children":[]},{"text":"From SQLite to Parquet: Processing Telegram Leaks for Fast Web Visualization","slug":"from-sqlite-to-parquet-processing-telegram-leaks-for-fast-web-visualization","level":"h2","children":[{"text":"The Challenge","slug":"the-challenge","level":"h3","children":[]},{"text":"The Workflow","slug":"the-workflow","level":"h3","children":[]},{"text":"The Frontend","slug":"the-frontend","level":"h3","children":[]},{"text":"Data Exploration Examples","slug":"data-exploration-examples","level":"h3","children":[]},{"text":"Building Custom Visualizations","slug":"building-custom-visualizations","level":"h3","children":[]}]}],"type":"post"}},"_links":{"self":"https://ejfox.com/blog/2025/paramilitary-telegram-leaks-001.json","html":"https://ejfox.com/blog/2025/paramilitary-telegram-leaks-001","index":"/blog.json"}}