NOTES
HOME
ESSAYS

NOTES

Commands cheatsheet

A list of all bot commands and what they do.

/about

Use this, followed by a description for your page, to craete an About section for your Stream.

Example usage: /about This is a really cool stream, trust me.

/clean

Sometimes, you might delete a Telegram message without first using the /delete command to remove the associated Drop from your Stream. This is when you need to use /clean, followed by the ID of the Drop (which you can get using the /mydrops command), to accomplish what /delete would.

Example usage: /clean 53901890342400359

/delete

Reply to a message with this command to remove it from your Stream. You can delete the message itself, but only after you’ve done this.

/editor

Returns a link to your theme editor, the page that lets you customise your Stream’s style using CSS.

/export

Returns the URL at which your ready-to-export Markdown data is available.

/faq

Returns a list of frequent user queries, and their answers.

/info

Will display information about the Streams project, and basic usage instructions.

/keyphrase

Returns the three word keyphrase that acts as authentication for private Streams, exports, and the theme-editor.

/layout

Follow this command with the name of a layout to set a new layout for your Stream.

Example usage: /layout feed

/layouts

Provides you with a list of layout names you can choose from.

/mydrops

Returns a list of your last ten Drops, with their associated IDs, usually used with the /clean command.

/newkeyphrase

Use this command if you want to change your keyphrase. Useful in cases where you feel like the security your keyphrase has been compromised. If you’ve only forgotten your keyphrase, use the /keyphrase command instead.

/newusername

Followed by your new username to chage the url location of your stream. Valid usernames need to be lower-case letters, and must not already be in use by another Stream.

Example usage: /newusername joodaloop

/protect

Reply to a Drop with this command to hide it from your public Stream. It will now only be available on your private Stream (your-url/username/keyphrase). Use /unprotect to bring it back to the public one.

/recover

Reply to the Telegram message associated with a deleted Drop to change it’s status to undeleted.

/rss

Returns a link to your Stream’s RSS feed.

/title

When you create your Stream, your username is used as the title for the Stream’s page. Use this command to set your own title.

Example usage: /title My Cool Stream

/theme

Follow this command with the name of a theme to set a new theme for your Stream.

Example usage: /theme notebook

/themes

Provides you with a list of themes you can choose from.

/unprotect

Reply to a drop with /unprotect to change it’s privacy status back to public and have it show up on your public Stream again.

/url

Returns the URL that your stream is currently located at.

Commands cheatsheet

A list of all bot commands and what they do.

/about

Use this, followed by a description for your page, to craete an About section for your Stream.

Example usage: /about This is a really cool stream, trust me.

/clean

Sometimes, you might delete a Telegram message without first using the /delete command to remove the associated Drop from your Stream. This is when you need to use /clean, followed by the ID of the Drop (which you can get using the /mydrops command), to accomplish what /delete would.

Example usage: /clean 53901890342400359

/delete

Reply to a message with this command to remove it from your Stream. You can delete the message itself, but only after you’ve done this.

/editor

Returns a link to your theme editor, the page that lets you customise your Stream’s style using CSS.

/export

Returns the URL at which your ready-to-export Markdown data is available.

/faq

Returns a list of frequent user queries, and their answers.

/info

Will display information about the Streams project, and basic usage instructions.

/keyphrase

Returns the three word keyphrase that acts as authentication for private Streams, exports, and the theme-editor.

/layout

Follow this command with the name of a layout to set a new layout for your Stream.

Example usage: /layout feed

/layouts

Provides you with a list of layout names you can choose from.

/mydrops

Returns a list of your last ten Drops, with their associated IDs, usually used with the /clean command.

/newkeyphrase

Use this command if you want to change your keyphrase. Useful in cases where you feel like the security your keyphrase has been compromised. If you’ve only forgotten your keyphrase, use the /keyphrase command instead.

/newusername

Followed by your new username to chage the url location of your stream. Valid usernames need to be lower-case letters, and must not already be in use by another Stream.

Example usage: /newusername joodaloop

/protect

Reply to a Drop with this command to hide it from your public Stream. It will now only be available on your private Stream (your-url/username/keyphrase). Use /unprotect to bring it back to the public one.

/recover

Reply to the Telegram message associated with a deleted Drop to change it’s status to undeleted.

/rss

Returns a link to your Stream’s RSS feed.

/title

When you create your Stream, your username is used as the title for the Stream’s page. Use this command to set your own title.

Example usage: /title My Cool Stream

/theme

Follow this command with the name of a theme to set a new theme for your Stream.

Example usage: /theme notebook

/themes

Provides you with a list of themes you can choose from.

/unprotect

Reply to a drop with /unprotect to change it’s privacy status back to public and have it show up on your public Stream again.

/url

Returns the URL that your stream is currently located at.

The API

Learn how to use the simple REST endpoints to fetch Drops to display on your own front-end, or post a new Drop from outside of Telegram.

Authentication

The GET endpoint requires no authentication and is available directly at the username/json route. It only returns a list of your public Drops, private ones stay protected.

For POST requests, your keyphrase is required to be added to the header of your request as an authentication method.

GET route (username/json)

Returns an array of all the user’s public Drops. Each element of the array corresponds to one Drop and has the following properties

PropertyContentsExample
timestampUNIX-style timestamp for when the Drop was created1681908281288
idA (shortened) UUID number for a DropMjZlNTgxMjAt
htmlHTML representation of the Drop’s contents, as rendered by the bot"<p>If you can see this message, it means that the POST endpoint works…</p>\n"
textThe raw text that the bot recieved from Telegram"If you can see this message, it means that the POST endpoint works..."
last_editUNIX-style timestamp for when the Drop was last edited1680172933000
mediaAn array of objects representing media associated with a drop[] (empty because no media)

There are three types of media objects that the media array might contain:

I. photo

{
    "type":"photo",
    "file_id":"AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ",
    "urlToFile":"https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ.jpg"
}

II. animation

{
    "type": "animation",
    "file_name": "mygif.gif",
    "file_id": "AgACAgUAAxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVaSsb-2Mkd7sgsrgADeQADLwQ",
    "urlToFile": "https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AgACAgUAAxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVaSsb-2Mkd7sgsrgADeQADLwQ.gif"
}

III. audio

{
    "type": "audio",
    "file_name": "mysong.mp3",
    "performer": "Judah",
    "title": "My Song",
    "file_id": "AxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVa-eQADLwsrht23tgs",
    "urlToFile": "https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVa-eQADLwsrht23tgs.ogg"
}

Example payload

To bring it all together, here’s an example response from a Stream that contains three Drops, one of which has a photo attached.

[
	{
		 "timestamp": "1681908281288",
		 "id": "MjZlNTgxMjAt", 
		 "html": "",
		 "text": "",
		 "last_edit": "",
		 "media": []
	},

	{
		 "timestamp": "1681908220941",
		 "id": "192jWiE9jneX", 
		 "html": "",
		 "text": "",
		 "last_edit": "",
		 "media": []
	},

	{
		 "timestamp": "1681901303130",
		 "id": "I093IE139jrn", 
		 "html": "",
		 "text": "",
		 "last_edit": "",
		 "media": [
			{
			    "type":"photo",
			    "file_id":"AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ",
			    "urlToFile":"https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ.jpg"
			}
		]
	}
]

You can use the JSON to render your Stream to your own site, or create your own copy of your data.

POST route (username/api/post)

Note: This endpoint has a rate-limit of 20 posts per minute.

Accepts a text string to store as a new Drop. The text for the new Drop should be added to a message entry in the request body.

You need to add keyphrase field to your headers object with your keyphrase as the value, in order to authenticate your request.

Example POST request:

fetch('https://streams.place/judah/api/post',
    {
    method: "POST",
    body: JSON.stringify({
        // This is the text of your post, markdown formatting is optional, of course. 
        message: `## This is a markdown formatted message.
        It's pretty **cool** to be able to do this, no?`,
        // This informs the bot that it should render this text as Markdown. (default: true)
        process_as_markdown: true,
        // This informs the bot to store this Drop as a private one. (default: false)
        protected: false

    }),
    headers: {
        "Content-Type": "application/json",
        // Add your own keyphrase instead of 'your-great-code'
        "keyphrase": "your-great-code"
    }

    }).then((response) => {
        if (response.ok) {
            return response.json();
        }
        throw new Error('Something went wrong');

    }).then((responseJson) => {
        console.log(responseJson)
        // responseJson = {success: true} if the request worked

    }).catch((error) => {
        console.log(error)
    });

Parameters

If process_as_markdown is set to true, the bot processes any markdown characters in the text to HTML.

If protected is set to true, the Drop will be saved as a private Drop.

Success tatus

If the request was successful, you should recieve a JSON payload with the success value set to true. If not, a value of false will return instead, along with a message value that informs you why the request failed.

Below are the error messages you might see, and explanations for what caused them:

I. Username is not being used by any existing users.

{ 
	"success": false, 
	"message": "Incorrect username. That user does not exist."
}

Please ensure that you are using the correct username (the sub-URL your Stream is located at). Example: if my Stream is located at https://streams.place/judah, my username is judah.

You can change your username with the /newusername command.

II. Keyprase did not match [username]’s keyphrase.

{ 
	"success": false, 
	"message": "Incorrect keyphrase. Go away."
}

Please make sure your keyphrase is correct, use the /keyphrase command to check your current keyphrase.

III. Something broke on the server.

{ 
	"success": false, 
	"message": "ERROR 500: Something went wrong."
}

If you aren’t trying something evil, this is probably my fault. Try again, and let me know if the error persists.

The API

Learn how to use the simple REST endpoints to fetch Drops to display on your own front-end, or post a new Drop from outside of Telegram.

Authentication

The GET endpoint requires no authentication and is available directly at the username/json route. It only returns a list of your public Drops, private ones stay protected.

For POST requests, your keyphrase is required to be added to the header of your request as an authentication method.

GET route (username/json)

Returns an array of all the user’s public Drops. Each element of the array corresponds to one Drop and has the following properties

PropertyContentsExample
timestampUNIX-style timestamp for when the Drop was created1681908281288
idA (shortened) UUID number for a DropMjZlNTgxMjAt
htmlHTML representation of the Drop’s contents, as rendered by the bot"<p>If you can see this message, it means that the POST endpoint works…</p>\n"
textThe raw text that the bot recieved from Telegram"If you can see this message, it means that the POST endpoint works..."
last_editUNIX-style timestamp for when the Drop was last edited1680172933000
mediaAn array of objects representing media associated with a drop[] (empty because no media)

There are three types of media objects that the media array might contain:

I. photo

{
    "type":"photo",
    "file_id":"AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ",
    "urlToFile":"https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ.jpg"
}

II. animation

{
    "type": "animation",
    "file_name": "mygif.gif",
    "file_id": "AgACAgUAAxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVaSsb-2Mkd7sgsrgADeQADLwQ",
    "urlToFile": "https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AgACAgUAAxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVaSsb-2Mkd7sgsrgADeQADLwQ.gif"
}

III. audio

{
    "type": "audio",
    "file_name": "mysong.mp3",
    "performer": "Judah",
    "title": "My Song",
    "file_id": "AxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVa-eQADLwsrht23tgs",
    "urlToFile": "https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AxkIOWHgngQsxhNo_nDpiRUc5Yp109yt284twnkwshVa-eQADLwsrht23tgs.ogg"
}

Example payload

To bring it all together, here’s an example response from a Stream that contains three Drops, one of which has a photo attached.

[
	{
		 "timestamp": "1681908281288",
		 "id": "MjZlNTgxMjAt", 
		 "html": "",
		 "text": "",
		 "last_edit": "",
		 "media": []
	},

	{
		 "timestamp": "1681908220941",
		 "id": "192jWiE9jneX", 
		 "html": "",
		 "text": "",
		 "last_edit": "",
		 "media": []
	},

	{
		 "timestamp": "1681901303130",
		 "id": "I093IE139jrn", 
		 "html": "",
		 "text": "",
		 "last_edit": "",
		 "media": [
			{
			    "type":"photo",
			    "file_id":"AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ",
			    "urlToFile":"https://fvinkehbbprjnyunhwkc.supabase.co/storage/v1/object/public/stream-images/beta/AgACAgUAAxkBAAIOIWQsxhNo_nDpiRUc5YpOZ6WbvU34AAK9tTEbx3xhVaSsb-2Mkd7zAQADAgADeQADLwQ.jpg"
			}
		]
	}
]

You can use the JSON to render your Stream to your own site, or create your own copy of your data.

POST route (username/api/post)

Note: This endpoint has a rate-limit of 20 posts per minute.

Accepts a text string to store as a new Drop. The text for the new Drop should be added to a message entry in the request body.

You need to add keyphrase field to your headers object with your keyphrase as the value, in order to authenticate your request.

Example POST request:

fetch('https://streams.place/judah/api/post',
    {
    method: "POST",
    body: JSON.stringify({
        // This is the text of your post, markdown formatting is optional, of course. 
        message: `## This is a markdown formatted message.
        It's pretty **cool** to be able to do this, no?`,
        // This informs the bot that it should render this text as Markdown. (default: true)
        process_as_markdown: true,
        // This informs the bot to store this Drop as a private one. (default: false)
        protected: false

    }),
    headers: {
        "Content-Type": "application/json",
        // Add your own keyphrase instead of 'your-great-code'
        "keyphrase": "your-great-code"
    }

    }).then((response) => {
        if (response.ok) {
            return response.json();
        }
        throw new Error('Something went wrong');

    }).then((responseJson) => {
        console.log(responseJson)
        // responseJson = {success: true} if the request worked

    }).catch((error) => {
        console.log(error)
    });

Parameters

If process_as_markdown is set to true, the bot processes any markdown characters in the text to HTML.

If protected is set to true, the Drop will be saved as a private Drop.

Success tatus

If the request was successful, you should recieve a JSON payload with the success value set to true. If not, a value of false will return instead, along with a message value that informs you why the request failed.

Below are the error messages you might see, and explanations for what caused them:

I. Username is not being used by any existing users.

{ 
	"success": false, 
	"message": "Incorrect username. That user does not exist."
}

Please ensure that you are using the correct username (the sub-URL your Stream is located at). Example: if my Stream is located at https://streams.place/judah, my username is judah.

You can change your username with the /newusername command.

II. Keyprase did not match [username]’s keyphrase.

{ 
	"success": false, 
	"message": "Incorrect keyphrase. Go away."
}

Please make sure your keyphrase is correct, use the /keyphrase command to check your current keyphrase.

III. Something broke on the server.

{ 
	"success": false, 
	"message": "ERROR 500: Something went wrong."
}

If you aren’t trying something evil, this is probably my fault. Try again, and let me know if the error persists.

The FAQ

No quotes, in the sense of quotetweeting. There’s no threading either, because you can just edit your Drop to add more words (until Telegram’s 48-hour edit limit ends).

You can however, link to Drops in two ways. The first is in-page linking, where clicking on a link will scroll down to the linked Drop. In-page links are located in the “Date” section of your Drops.

Permalinks for Drops also exist, in the form of a unique streams.place/username/drops/drop-id link for each one. Those are found in the “Time” section of the Drop.

Why don’t Streams also have a web dashboard to edit/delete posts?

Because the point of a Stream is low-friction, minimal effort posts. I want you to show your rough work, your outtakes and footnotes. The need to log in, visit a website, click different buttons, etc. are all obstacles to just posting.

If you’re using a Stream, it’s best to accept the light, unserious nature of the medium from the get-go. Posting here should feel effortless. If it feels like you’re fighting the UI, it’s probably better for you to export your data and try someplace else.

If you just want to create a minimal blog, check out bear.blog. If you want a regular microblogging platform, check out micro.blog or Bluesky.

Why isn’t there support for videos?

I don’t want to pay to host really large media files. The site allows media embeds (including YouTube and Vimeo), you can always use those to add videos. Or you can convert videos to GIFs, the bot accepts those.

What’s the encryption and security of how data goes from my Telegram chat to my Stream?

Data is stored on Telegram’s servers, and then sent to the bot, which lives on a Digital Ocean droplet. Encryption and security are taken care of by Telegram and the bot’s token.

Can Streams be made private?

No, but you can hide specific Drops from your public Stream by using the /protect command, just reply to a Drop with /protect. Your private Stream (including protected Drops) is available on a private page (your-url/keyphrase).

However, if you really want to hide your entire Stream, just changing your username to a long, weird string will probably be enough to make sure nobody will find it.

Is this free? How? Why?

Yes, it is. Firstly, becasue it was fairly easy to build, just under month of work. Second, at the moment, it costs me close to nothing to keep it running.

However, “close to nothing” doesn’t mean $0. Labour, servers and domains aren’t exactly free. And so you can, of course, choose to send me money if you feel like it.

Will it always be free?

Always is a strong word. I’m not even sure how long Telegram will be around for. But all your data is stored in it’s original format and will be avilable to export whenever you wish to do so.

It’s possible that there will one day be a paid tier that will provide things like higher quality images, video hosting, etc. But the most important bits will be free.

What did you use to make this?

The bot runs on a DigitalOcean droplet, and listens for messages that are then stored in a PostgresDB and S3 Bucket on Supabase. It uses the excellent Telegraf and Marked libraries within an ExpressJS server.

Like all my projects, the front-end is plain ol’ HTML+CSS, with full-text search being provided by a simple JavaScript function. I plan to make the code for recieving and processing markdown-formatted mesages from a Telegram open-source soon.

The FAQ

No quotes, in the sense of quotetweeting. There’s no threading either, because you can just edit your Drop to add more words (until Telegram’s 48-hour edit limit ends).

You can however, link to Drops in two ways. The first is in-page linking, where clicking on a link will scroll down to the linked Drop. In-page links are located in the “Date” section of your Drops.

Permalinks for Drops also exist, in the form of a unique streams.place/username/drops/drop-id link for each one. Those are found in the “Time” section of the Drop.

Why don’t Streams also have a web dashboard to edit/delete posts?

Because the point of a Stream is low-friction, minimal effort posts. I want you to show your rough work, your outtakes and footnotes. The need to log in, visit a website, click different buttons, etc. are all obstacles to just posting.

If you’re using a Stream, it’s best to accept the light, unserious nature of the medium from the get-go. Posting here should feel effortless. If it feels like you’re fighting the UI, it’s probably better for you to export your data and try someplace else.

If you just want to create a minimal blog, check out bear.blog. If you want a regular microblogging platform, check out micro.blog or Bluesky.

Why isn’t there support for videos?

I don’t want to pay to host really large media files. The site allows media embeds (including YouTube and Vimeo), you can always use those to add videos. Or you can convert videos to GIFs, the bot accepts those.

What’s the encryption and security of how data goes from my Telegram chat to my Stream?

Data is stored on Telegram’s servers, and then sent to the bot, which lives on a Digital Ocean droplet. Encryption and security are taken care of by Telegram and the bot’s token.

Can Streams be made private?

No, but you can hide specific Drops from your public Stream by using the /protect command, just reply to a Drop with /protect. Your private Stream (including protected Drops) is available on a private page (your-url/keyphrase).

However, if you really want to hide your entire Stream, just changing your username to a long, weird string will probably be enough to make sure nobody will find it.

Is this free? How? Why?

Yes, it is. Firstly, becasue it was fairly easy to build, just under month of work. Second, at the moment, it costs me close to nothing to keep it running.

However, “close to nothing” doesn’t mean $0. Labour, servers and domains aren’t exactly free. And so you can, of course, choose to send me money if you feel like it.

Will it always be free?

Always is a strong word. I’m not even sure how long Telegram will be around for. But all your data is stored in it’s original format and will be avilable to export whenever you wish to do so.

It’s possible that there will one day be a paid tier that will provide things like higher quality images, video hosting, etc. But the most important bits will be free.

What did you use to make this?

The bot runs on a DigitalOcean droplet, and listens for messages that are then stored in a PostgresDB and S3 Bucket on Supabase. It uses the excellent Telegraf and Marked libraries within an ExpressJS server.

Like all my projects, the front-end is plain ol’ HTML+CSS, with full-text search being provided by a simple JavaScript function. I plan to make the code for recieving and processing markdown-formatted mesages from a Telegram open-source soon.