Perhaps you'd like to automate some aspect of posting on cohost. Here is a rough guide of the things I've learned trying to do just that. This guide covers:
- A list of client libraries that exist so far that you can use
- Logging in
- Creating, editing, and deleting posts
- Uploading attachments
This guide is written for people who know their way around sending arbitrary HTTP requests and perhaps have a library that they like in their language of choice for doing so. (By the way: this is all stuff mostly learned from popping open the web developer tools in my browser and hitting the Network tab!)
Some disclaimers: I don't represent any official source of knowledge on using cohost. The API is currently undocumented, unsupported, and likely to change without warning. Be nice and stay out of trouble!
Client libraries
There really aren't very many to speak of yet, but if you find (or write) one for your language go ahead and leave a comment and I'll likely update this list!
- Node.js: cohost.js by @mog (logging in, post creation, listing authorized pages, listing a user's posts, listing notifications, etc)
- Rust: eggbug-rs by @iliana (logging in, post creation, attachment uploads, probably not adding much else)
Logging in
cohost currently does not have long-lived API keys, so you need to use the same login endpoint all the non-automated users use. Sessions last for about one week and there doesn't appear to be a renewal mechanism, so you'll likely need to store your login credentials somewhere your code can get to them. Secure storage of the email + password pair is an exercise left to the developer.
(As an alternative: you might consider making a separate login and emailing support@cohost.org to get it added as someone who can post to that page. That may be a taller ask while the activation queue is quite long, though.)
cohost uses client-side password hashing, presumably combined with additional server-side hashing. This means that the process to log in is:
-
Get the salt for an email. Send a GET request to
https://cohost.org/api/v1/login/salt?email=:email, replacing:emailwith the email address.This responds with a single-member JSON object that looks like
{"salt":"FAuw-Z1I_IWfY4UMaW4kig"}. The salt is a Base64-encoded string using the URL-safe alphabet and no padding. -
Perform the client-side password hash (currently: PBKDF2 with HMAC-SHA-384, 200,000 iterations, 128-byte key length) with the salt you retrieved in step 1. Most language library ecosystems have a PBKDF2 library available of some kind. (Thanks to @mog's code demonstrating these variables!)
Important: This hash is equivalent to your actual password and can be used to log in as you. Don't share it and don't log it!
-
Send a POST request to
https://cohost.org/api/v1/loginwith two values in the request body (this can be either JSON or application/x-www-form-urlencoded or possibly even multipart/form-data):email: the user's emailclientHash: the Base64-encoded output of the hash in step 2, encoded with the standard Base64 alphabet and padding (note the difference in encoding scheme from the salt)
When correctly encoded the Base64-encoded
clientHashis 172 bytes long and ends with a single=.
Once you've done this dance and you get a successful response code, you should get a set-cookie header from the server. Right now it's a cookie named connect.sid which is an implementation detail of presumably express-session. It's probably easiest to use an HTTP client library that can store and re-use cookies automatically. The response also contains your user ID, which you don't need for anything below.
Creating posts
This is the easy part. Once you're logged in, you can send a POST request to https://cohost.org/api/v1/project/:project/posts (what show as "pages" in the UI are called "projects" in the internals; replace :project with the name of your page, such as iliana-test) with a JSON body that looks like:
{
"adultContent": false,
"blocks": [
{
"markdown": {
"content": "wow\n\nwow\n\nwow\n\nwowwwwwww"
},
"type": "markdown"
}
],
"cws": [],
"headline": "cool post!!",
"postState": 1,
"tags": []
}
This endpoint responds with the post ID wrapped in JSON:
{"postId":45731}
And here's the post that request resulted in. Most of this is self-explanatory, but we'll look more closely at blocks and postState.
blocks is an array of, well, blocks. There are two types of blocks that I've seen: markdown and attachment. We'll cover attachment in the next section. While I recommend only sending a single markdown block for simplicity, the post editor actually splits your post into multiple markdown blocks split on \n\n.
postState, as far as I can tell, is 0 when you want to post a draft and 1 when you want to publish your post. It might have other (administrative?) states but I haven't seen them.
Important: Log your post ID! If something goes wrong, you'll need to know the post ID to delete it. I've run into some issues sending malformed posts which happily get inserted into the database and then fail to render for anyone who follows that account.
Editing posts
You send the same request body as creating a post, but as a PUT request to /project/:project/posts/:postId, replacing :project and :postId. A nice chance for some code re-use.
Deleting posts
Send a DELETE request to /project/:project/posts/:postId; no body required. This is particularly useful to know in case you break the site somehow.
Uploading attachments
Alright hold onto your butts. Uploading attachments is a five-step process:
- Create a post with placeholder attachment IDs.
- Ask cohost for some credentials to upload an attachment.
- Upload the attachment directly to the CDN.
- Tell cohost you're done uploading the attachment.
- Update the post with your new attachment IDs.
Attachments step 1: Create a post with placeholder attachment IDs
To create this post you use attachment blocks. That looks like this (using the POST /api/v1/projects/:project/posts endpoint):
{
"adultContent": false,
"blocks": [
{
"attachment": {
"attachmentId": "00000000-0000-0000-0000-000000000000"
},
"type": "attachment"
}
],
"cws": [],
"headline": "cool post!!",
"postState": 1,
"tags": []
}
You'll notice the attachmentId is "nil" (all zeroes), which indicates the attachment is pending upload.
Attachments step 2: Ask cohost for some credentials to upload an attachment
Time for step 2: asking cohost to upload an attachment. Send a POST to /api/v1/projects/:project/posts/:postId/attach/start with the following values:
filename: a filename for the filecontentType: the MIME content-type for the file, such as image/pngcontentLength: the length of the file in bytes
The response will be a JSON document with these fields:
attachmentId: the attachment ID. Save this!url: the URL to upload the file torequiredFields: the required fields, in addition to the file, to submit to the URL (this contains a signature from the cohost service)
Attachments step 3: Upload the attachment directly to the CDN
The url you received in step 2 is an endpoint provided by the CDN service for uploading a file. The reverse engineering I've done indicates that you need to send a multipart/form-data body to this URL containing all the data from the requiredFields you received in step 2, as well a field named file with the file contents itself.
Attachments step 4: Tell cohost you're done uploading the attachment
This is done by sending a POST to /api/v1/project/:project/posts/:postId/attach/finish/:attachmentId with no body. In case you need it, this returns a JSON document with a url field containing the final CDN URL to retrieve the file; you can use this in the markdown blocks directly if you like.
Attachments step 5: Update the post with your new attachment IDs
This is the same as editing a post (PUT /api/v1/project/:project/posts/:postId), but now you're just filling in the attachmentId fields with whatever you ended up getting in step 2 (or alternately removing all the attachment blocks and putting the images in your markdown directly).
And that's all I know!
If you need some sample code to follow along to, you can check out eggbug-rs. I hope that it is not terribly confusing to non-Rust programmers.
ok bye
[updated on 2022-10-28: figured i'd fix this guide for the attachment change announced on august 1]
