{"id":13408658,"url":"https://github.com/dkaslovsky/thread-safe","last_synced_at":"2026-01-19T15:07:29.004Z","repository":{"id":65150589,"uuid":"571351979","full_name":"dkaslovsky/thread-safe","owner":"dkaslovsky","description":"Keep your favorite Twitter threads safe with a local copy","archived":false,"fork":false,"pushed_at":"2023-01-30T14:15:10.000Z","size":7002,"stargazers_count":32,"open_issues_count":0,"forks_count":2,"subscribers_count":4,"default_branch":"main","last_synced_at":"2024-07-31T20:31:34.476Z","etag":null,"topics":["downloader","local","scraper","twitter"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dkaslovsky.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2022-11-27T23:29:52.000Z","updated_at":"2024-07-25T14:59:56.000Z","dependencies_parsed_at":"2023-02-01T04:45:36.428Z","dependency_job_id":null,"html_url":"https://github.com/dkaslovsky/thread-safe","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkaslovsky%2Fthread-safe","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkaslovsky%2Fthread-safe/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkaslovsky%2Fthread-safe/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dkaslovsky%2Fthread-safe/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dkaslovsky","download_url":"https://codeload.github.com/dkaslovsky/thread-safe/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":243584406,"owners_count":20314751,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["downloader","local","scraper","twitter"],"created_at":"2024-07-30T20:00:54.403Z","updated_at":"2026-01-19T15:07:23.973Z","avatar_url":"https://github.com/dkaslovsky.png","language":"Go","funding_links":[],"categories":["\u003ca name=\"backup\"\u003e\u003c/a\u003eBackup"],"sub_categories":[],"readme":"# thread-safe\nKeep your favorite Twitter threads safe by downloading a local copy\n\n\u003c/br\u003e\n\n## Table of Contents\n- [Overview](#overview)\n- [Example](#example)\n- [Installation](#installation)\n  - [Releases](#releases)\n  - [Installing from source](#installing-from-source)\n- [Usage](#usage)\n  - [Configuration](#configuration)\n  - [Top Level](#top-level)\n  - [Subcommands](#subcommands)\n  - [Custom CSS](#custom-css)\n  - [Custom Templates](#custom-templates)\n- [License](#license)\n\n\u003c/br\u003e\n\n## Overview\n`thread-safe` is a simple CLI for saving a local copy of a Twitter thread.\n\nSpecifically, `thread-safe` generates an HTML file containing all of a thread's contents including each tweet's text, links, and media attachments (images, videos). This file, all attachments, and a JSON data file are saved to the local filesystem and the HTML can be used to display the thread locally in a browser at any time.\n\nBy using a dedicated directory for all generated files, `thread-safe` can be used to maintain a local library of saved threads. Thread names are specified by the user as CLI arguments and standard commandline tooling (e.g., `grep`, `find`, `fzf`, etc) can be used to search the library for saved content.\n\n`thread-safe` is designed to\n* Save a local copy of informative Twitter threads\n* Eliminate the need to use an external or third-party app to which you need to grant access to your Twitter account\n* Eliminate the need to reply to a tweet to \"unroll\" or otherwise save the thread's contents\n* Ignore replies from users other than the thread's author\n\nTherefore, `thread-safe`'s definition of a thread is intentionally limited to\n\u003e _a consecutive series of tweets authored by the same Twitter user_. \n\n\u003c/br\u003e\n\n## Example\nTo demonstrate typical usage, we've identified a Twitter thread of [Nathan MacKinnon hockey highlights](https://twitter.com/Avalanche/status/969990878944149504) from 2018 that we simply must preserve with a local copy. This thread is great not only for its content but also because it contains both images and video. The thread contains eight tweets before any non-author replies and the URL of the last tweet URL is `https://twitter.com/Avalanche/status/969990907490484225`.\n\nWe'll name our local copy of this thread `Nathan MacKinnon 2018` and save it by running\n```\n$ thread-safe save \"Nathan MacKinnon 2018\" \"https://twitter.com/Avalanche/status/969990907490484225\"\n```\nor, equivalently,\n```\n$ thread-safe save \"Nathan MacKinnon 2018\" 969990907490484225\n```\n\nWe now have a `thread.html` file in the `$THREAD_SAFE_PATH/Nathan_MacKinnon_2018` directory with the all of the thread's contents:\n\nhttps://user-images.githubusercontent.com/20505301/210184312-dcc6be97-3d1f-4fff-ba0e-36cd00f52add.mov\n\nNote that we ran `thread-safe` with the default HTML template and no CSS. If we later wish to regenerate `thread.html` with a specified template and CSS, we can run\n```\n$ thread-safe regen --template path/to/template --css path/to/css \"Nathan MacKinnon 2018\"\n```\nand the file will be rewritten using the target template and CSS files. We also could have provided the `--template` and `--css` flags to the original `thread-safe save` command.\n\nAll resulting thread files can be found in this repository's [examples](examples) directory.\n\n\u003c/br\u003e\n\n## Installation\n`thread-safe` can be installed by downloading a prebuilt binary or by the go get command.\n\n\u003c/br\u003e\n\n### Releases\nThe recommended installation method is downloading the latest released binary.\nDownload the appropriate binary for your operating system from this repository's [releases](https://github.com/dkaslovsky/thread-safe/releases/latest) page or via `curl`.\n\nFor example, to download the arm64 binary for macOS via curl run\n```\n$ curl -o thread-safe -L https://github.com/dkaslovsky/thread-safe/releases/latest/download/thread-safe_darwin_arm64\n```\nA similar path is used for other operating systems and architectures.\n\n\u003c/br\u003e\n\n### Installing from Source\n`thread-safe` can also be installed using Go's built-in tooling:\n```\n$ go install github.com/dkaslovsky/thread-safe@latest\n```\nBuild from source by cloning this repository and running `go build`.\n\n\u003c/br\u003e\n\n## Usage\n`thread-safe` is lightweight and simple to use. To save a thread, two items are needed:\n* A valid [Twitter API bearer token](https://developer.twitter.com/en/docs/authentication/oauth-2-0/bearer-tokens)\n* The URL or ID of the **last** tweet in the thread\n\nA tweet's URL is typically of the form\n\u003e`https://twitter.com/\u003cusername\u003e/status/\u003cID\u003e?\u003cparameters\u003e`\n\nThe entire URL or simply the numeric `\u003cID\u003e` portion of the URL can be provided as an argument to specify a tweet.\n\nWhile it is inconvenient to have to identify the last tweet in the thread rather than the more natural first tweet, limitations of the Twitter API make this unavoidable for `thread-safe`'s workflow.\n\n\u003c/br\u003e\n\n### Configuration\n#### API Bearer Token\nThe [Twitter API bearer token](https://developer.twitter.com/en/docs/authentication/oauth-2-0/bearer-tokens) can be set either in a configuration file `${HOME}/.thread-safe` using the convention\n```\ntoken = \u003ctoken value\u003e\n```\nor using the `THREAD_SAFE_TOKEN` environment variable, which will override any value set in the configuration file.\n\n#### Output Path\nOutput files will be written to either the directory specified by `THREAD_SAFE_PATH` or the current directory if this environment variable is not set.\n\n\n\u003c/br\u003e\n\n### Top Level\n```\n$ thread-safe --help\n'thread-safe' saves a local copy of a Twitter thread\n\nUsage:\n  thread-safe [flags]\n  thread-safe [command]\n\nAvailable Commands:\n  save     saves thread content and generates a local html file\n  regen    regenerates an html file from a previously saved thread\n\nFlags:\n  -h, --help\t help for thread-safe\n  -v, --version\t version for thread-safe\n\nEnvironment Variables:\n  THREAD_SAFE_PATH\ttop level path for thread files (current directory if unset)\n  THREAD_SAFE_TOKEN\tbearer token for Twitter API (overrides value read from \"${HOME}/.thread-safe\" if set)\n\nUse \"thread-safe [command] --help\" for more information about a command\n```\n\n\u003c/br\u003e\n\n### Subcommands\n* `save`: save thread data and generate HTML for local browsing\n```\n$ thread-safe save --help\n'save' saves thread content and generates a local html file\n\nUsage:\n  thread-safe save [flags] \u003cname\u003e \u003clast-tweet\u003e\n\nArgs:\n  name           string  name to use for the thread\n  last-tweet     string  URL or ID of the last tweet in a single-author thread\n\nFlags:\n  -c, --css             string  optional path to CSS file\n  -t, --template        string  optional path to template file\n      --no-attachments          do not download attachments\n\nEnvironment Variables:\n  THREAD_SAFE_PATH\ttop level path for thread files (current directory if unset)\n  THREAD_SAFE_TOKEN\tbearer token for Twitter API (overrides value read from \"${HOME}/.thread-safe\" if set)\n```\n\n* `regen`: reprocess saved thread data using an updated template or CSS\n```\n$ thread-safe regen --help\n'regen' regenerates an html file from a previously saved thread\n\nUsage:\n  thread-safe regen [flags] \u003cname\u003e\n\nArgs:\n  name  string  name given to the thread\n\nFlags:\n  -c, --css       string  optional path to CSS file\n  -t, --template  string  optional path to template file\n\nEnvironment Variables:\n  THREAD_SAFE_PATH\ttop level path for thread files (current directory if unset)\n  THREAD_SAFE_TOKEN\tbearer token for Twitter API (overrides value read from \"${HOME}/.thread-safe\" if set)\n```\n\u003c/br\u003e\n\n### Custom CSS\nThe `save` and `regen` subcommands support providing an optional path to a CSS file to be linked as an external stylesheet in the generated HTML.\n\nIf a CSS file is not specified, `thread-safe` will attempt to use `${THREAD_SAFE_PATH}/thread-safe.css` as a default. This allows default specification of a global CSS file across all saved threads. The HTML will be generated without CSS if no such file exists.\n\n\u003c/br\u003e\n\n### Custom Templates\nThe `save` and `regen` subcommands also support providing an optional path to a file containing an HTML template to be used in place of `thread-safe`'s default template. The contents of a provided template file must be parsable by the Go [(*Template).Parse()](https://pkg.go.dev/text/template#Template.Parse) function.\n\nThe template must make use of the following objects:\n\n* The top level `TemplateThread` object defined by\n```go\ntype TemplateThread struct {\n\tName   string          // Name of thread\n\tHeader string          // Thread header information\n\tTweets []TemplateTweet // Thread's tweets\n}\n```\n* The nested `TemplateTweet` object defined by\n```go\ntype TemplateTweet struct {\n\tText        string               // Tweet's text content\n\tAttachments []TemplateAttachment // Tweet's media attachments\n}\n```\n* The `TemplateAttachment` object defined by\n```go\ntype TemplateAttachment struct {\n\tPath string // Path to the attachment file on the local filesystem\n\tExt  string // Attachment's extension (.jpg, .mp4)\n}\n\nfunc (TemplateAttachment) IsImage() bool\n\nfunc (TemplateAttachment) IsVideo() bool\n```\n\nA custom template may specify a placeholder for a CSS file by using the `%s` format verb.\nFor example,\n```html\n\u003chead\u003e\n\u003clink rel=\"stylesheet\" type=\"text/css\" href=\"%s\" media=\"screen\" /\u003e\n\u003c/head\u003e\n```\nis used in the default template to inject a specified CSS file path in place of the `%s` verb.\nNote that CSS file path will be injected in place of the _first_ occurrence of `%s` verb.\n\nIf a template file is not specified, `thread-safe` will attempt to use `${THREAD_SAFE_PATH}/thread-safe.tmpl` as a default. The HTML will be generated using the predefined default template if no such file exists.\n\n\u003c/br\u003e\n\n## License\n`thread-safe` is released under the [MIT License](./LICENSE).\nDependency licenses are available in this repository's [CREDITS](./CREDITS) file.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkaslovsky%2Fthread-safe","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdkaslovsky%2Fthread-safe","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdkaslovsky%2Fthread-safe/lists"}