cohost

Unofficial Common Lisp client library for Cohost
git clone https://todayiwilllaunchmyinfantsonintoorbit.com/cohost.git
Log | Files | Refs | LICENSE

commit 62594deeb4aa9e858ac3cf666b7d337d13809076
parent 971041e188093edbc8a54adc3f1ab45a81e7c28b
Author: Decay <decay@todayiwilllaunchmyinfantsonintoorbit.com>
Date:   Sun, 12 Feb 2023 18:27:55 -0800

Make documentation into real documentation

Diffstat:
MREADME.markdown | 278+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
1 file changed, 252 insertions(+), 26 deletions(-)

diff --git a/README.markdown b/README.markdown @@ -1,6 +1,16 @@ -# Cohost +# Cohost for Common Lisp -## Installation +### Decay <decay@todayiwilllaunchmyinfantsonintoorbit.com> + +This is an unofficial API client library for cohost.org, suitable for use in bots or other non-web client software. At present, it is built on top of the reverse-engineered "V1" API used by the Cohost website itself, and is also in early development. In other words, it's super unstable, anything can change at any time, and lots of things may not work. If it breaks, you get to keep both halves. However: + +**This is the canonical documentation and is expected to be accurate. Any functionality that doesn't match the specification here is a bug. I am not hiding any secret knowledge in Discord, Telegram, Matrix or any other cursed chat systems that people try to use as document repos. You can report any bugs or send any pull requests to me via email.** + +## License + +The STRONGEST PUBLIC LICENSE + +## Usage Fetch this into your ASDF system directory (typically ~/common-lisp/): @@ -12,40 +22,256 @@ $ git clone https://todayiwilllaunchmyinfantsonintoorbit.com/cohost.git Then in your favorite CL implementation, load it and its dependencies via Quicklisp: ``` -(ql:quickload 'cohost) +CL-USER> (ql:quickload 'cohost) ``` -## Usage +### REPL Examples + +Load the library and create a client: + +``` +CL-USER> (ql:quickload 'cohost) +(COHOST) +CL-USER> (defvar *cohost* (cohost:init-client)) +*COHOST* +``` + +Login with standard user credentials: + +``` +CL-USER> (cohost:login *cohost* "decay@todayiwilllaunchmyinfantsonintoorbit.com" "hunter2") +"someopaquelogintoken" +``` -The COHOST package contains the main public interface: +Login with the token returned from an earlier LOGIN: -**(COHOST:INIT-CLIENT)** - Returns a client object -**(COHOST:LOGIN [client] [email address] [password])** - Logs the client in to Cohost -**(COHOST:SIMPLE-POST [logged-in client] [project name to post to] [headline of post] [Markdown post contents] &KEY (:SHARE-OF [post id]) (:ADULT-CONTENT [boolean]) (:DRAFT [boolean]) (:TAGS [list of post tags]) (:CONTENT-WARNINGS [list of CWs]))** - Simple interface to create Markdown/text-only posts without having to go through the more complex mechanisms in COHOST.CLIENT. Returns a COHOST.CLIENT:CHOST object identical to the one posted to Cohost. -**(COHOST:NEW-POST [logged-in client] [project name to post to] &KEY (:ID [post ID, if this models an existing post]) (:BLOCKS [list of constructed BLOCK objects specifying post components]) (:HEADLINE [headline of post]) (:SHARE-OF [post id]) (:ADULT-CONTENT [boolean]) (:DRAFT [boolean]) (:TAGS [list of post tags]) (:CONTENT-WARNINGS [list of CWs])** - Similar in many respects to COHOST:SIMPLE-POST but consumes a preconstructed list of CONTENT-BLOCKs instead of a simple Markdown string. This fully supports creating a CHOST object with attachments. -**(COHOST:NEW-ATTACHMENT [logged-in client] [attachment id] &KEY (:ALT-TEXT [attachment alt text]))** - Create a new object for an *existing* post attachment. Not generally useful for basic post-only cases; if you want to *upload* an attachment, see the next function. Returns an ATTACHMENT CONTENT-BLOCK object. -**(COHOST:NEW-FILE-ATTACHMENT [logged-in client] [Lisp PATHNAME to file to upload] &KEY (:ALT-TEXT [attachment alt text]) (:FILENAME [filename to use for the upload, defaults to (FILE-NAMESTRING [pathname])]) (:CONTENT-TYPE [content-type of file, defaults to file's MIME type as determined by the TRIVIAL-MIMES package]))** - Create a new object for a new file attachment. Will be automatically uploaded and attached to the associated post at post time. Typically it's not necessary or useful to specify CONTENT-TYPE or FILENAME; if you're doing complicatd things that require you pass these explicitly, you'll know. Returns a FILE-ATTACHMENT CONTENT-BLOCK object. -**(COHOST:NEW-MARKDOWN-BLOCK [logged-in client] [Markdown content])** - Returns a MARKDOWN CONTENT-BLOCK object containing the specified content. -**(COHOST:COPY-POST [client] [post object]) - Returns a full deep copy of the specified post object that shares no structure with the original object, including fresh copies of all CONTENT-BLOCKs. -**(COHOST:POST [logged-in client] [post object]) - Posts a fully constructed post object with all attachments. If the post object passed in has a post ID set, will update the specified post, otherwise creates a new one. Returns a new CHOST object with ID populated and with any FILE-ATTACHMENT CONTENT-BLOCKs transformed into ATTACHMENTs containing the correct ID. +``` +CL-USER> (cohost:login-with-token *cohost* "foo") +#<DRAKMA:COOKIE-JAR (with 1 cookie) {1004380433}> +``` -Every class has a full set of exported accessors that also serve as SETF places. Each accessor corresponds to a constructor parameter above but some have function names different from the parameters above intended to avoid collisions or ambiguity: +Create a simple post: + +``` +CL-USER> (cohost:simple-post *cohost* "DecayWTF" "Simple Test Post" "This is a *simple* test post!" :tags '("lisp" "bot")) +#<COHOST.CLIENT::CHOST {100391EDA3}> +``` -**CHOST** - Accessors: POST-ID, PROJECT, ADULT-CONTENT, CONTENT-BLOCKS, CONTENT-WARNINGS, DRAFT, HEADLINE, TAGS, SHARE-OF -**ATTACHMENT** - Accessors: ALT-TEXT, ATTACHMENT-ID -**FILE-ATTACHMENT** - Accessors: ALT-TEXT, ATTACHMENT-PATHNAME, ATTACHMENT-FILENAME, ATTACHMENT-CONTENT-TYPE -**MARKDOWN** - Accessors: CONTENT +Create a CHOST object, construct it with both Markdown and an attachment and post it: -### Example Usage +``` +CL-USER> (let ((post (cohost:new-post *cohost* "DecayWTF" :headline "Test Post")) + (content (cohost:new-markdown-block *cohost* "This is the *markdown* content of my test post!")) + (pic (cohost:new-file-attachment *cohost* #p"~/Pictures/10f.gif"))) + (setf (cohost:content-blocks post) (list pic content)) + (cohost:post *cohost* post)) +#<COHOST.CLIENT::CHOST {1003646653}> +``` -## Author +Get a post by ID: -* Decay (decay@todayiwilllaunchmyinfantsonintoorbit.com) +``` +CL-USER> (defvar *post-content* (cohost:get-post *cohost* 405148)) +*POST-CONTENT* +``` -## Copyright +Get the current dashboard for the logged-in user's default project: -Copyright (c) 2022 Decay (decay@todayiwilllaunchmyinfantsonintoorbit.com) +``` +CL-USER> (defvar *posts* (cohost:get-dash *cohost*)) +*POSTS* +``` -## License +## Public API + +The full public API is exported from the COHOST package. Right now nothing is fully stable; in particular, +GET-POST, GET-DASH and other such functions will eventually return CLOS objects instead of more-or-less the raw result of deserializing JSON. The interfaces to post-creation functions (SIMPLE-POST, NEW-POST, NEW-MARKDOWN-BLOCK, NEW-FILE-ATTACHMENT and so forth) that already operate on CLOS objects *can* be expected to be relatively stable, however. Existing class names (eg CHOST, BLOCK, and so on) can also be expected to be stable. + +All boolean values are [generalized booleans](http://clhs.lisp.se/Body/26_glo_g.htm#generalized_boolean) as defined in the X3J13 spec. All non-NIL values are considered true. + +### Setup/Teardown Functions + +* `(INIT-CLIENT)` - Initializes the client + + **Returns**: A client object to pass to other functions. + +* `(LOGIN CLIENT EMAIL PASSWORD)` - Login with user credentials + + **Parameters**: + + * CLIENT - Initialized client object + * EMAIL - String email address + * PASSWORD - String password + + **Returns**: An opaque string authentication token that can be passed to LOGIN-WITH-TOKEN for later sessions. + + **Side Effects**: Logs the CLIENT into Cohost. + +* `(LOGIN-WITH-TOKEN CLIENT TOKEN)` - Login with authentication token + + **Parameters**: + + * CLIENT - Initialized client object + * TOKEN - An opaque authentication token returned from a previous call to LOGIN. + + **Side Effects**: Logs the CLIENT into Cohost. + + **Notes**: Token expiration is hardwired into the token (which is currently just the contents of the "connect.sid" cookie) so it will expire eventually; logins via either LOGIN or LOGIN-WITH-TOKEN can expire at any time, including in the middle of a session. LOGIN-WITH-TOKEN currently doesn't do any verification that the token is not expired. + +### Post creation/manipulation + +* `(NEW-POST CLIENT PROJECT &KEY ID HEADLINE DRAFT ADULT-CONTENT BLOCKS CONTENT-WARNINGS TAGS SHARE-OF)` - Create a new CHOST object and optionally set parameters or content. + + **Parameters** + + * CLIENT - Initialized client object + * PROJECT - String name of project to post as. Must be a project that the logged-in user has rights to post as. + * ID - The ID of an existing post. This is useful to alter an existing post; if ID is set, POST will edit the specified post ID rather than create a new post. Default NIL. + * HEADLINE - A string specifying the post headline. Default NIL (no headline). + * DRAFT - If true, create the new post as a draft. Default NIL. + * ADULT-CONTENT - If true, marks the post as 18+. Default NIL. + * BLOCKS - A list of CONTENT-BLOCK objects (more information below) specifying the post content. MARKDOWN CONTENT-BLOCKs are posted as sequential paragraphs, while ATTACHMENT CONTENT-BLOCKs (either ATTACHMENT or FILE-ATTACHMENT) post as image attachments at the top of the post, in the order they appear in the list. Default empty (empty post content). + * CONTENT-WARNINGS - A list of strings specifying content warnings to attach to the post. Default empty. + * TAGS - A list of strings specifying post tags. Default empty. + * SHARE-OF - The ID of an existing post. If set, the post will be created as a reply to the specificed post. Default NIL. + + **Returns**: A CHOST object as defined by the parameters. + + **Accessors**: + + * `(PROJECT CHOST)`/`(SETF (PROJECT CHOST) NEW-PROJECT)` - Get or set the PROJECT of CHOST. + * `(ID CHOST)`/`(SETF (ID CHOST) NEW-ID)` - Get or set the ID of CHOST. + * `(HEADLINE CHOST)`/`(SETF (HEADLINE CHOST) NEW-HEADLINE)` - Get or set the HEADLINE of CHOST. + * `(DRAFT CHOST)`/`(SETF (DRAFT CHOST) DRAFTP)` - Get or set the DRAFT state of CHOST. + * `(ADULT-CONTENT CHOST)`/`(SETF (ADULT-CONTENT CHOST) ADULT-CONTENT-P)` - Get or set the ADULT-CONTENT state of CHOST. + * `(CONTENT-BLOCKS CHOST)`/`(SETF (CONTENT-BLOCKS CHOST) NEW-BLOCKS)` - Get or set the CONTENT-BLOCKS list of CHOST. + * `(CONTENT-WARNINGS CHOST)`/`(SETF (CONTENT-WARNINGS CHOST) NEW-CWS)` - Get or set the CONTENT-WARNINGS list of CHOST. + * `(TAGS CHOST)`/`(SETF (TAGS CHOST) NEW-TAGS)` - Get or set the TAGS list of CHOST. + * `(SHARE-OF CHOST)`/`(SETF (SHARE-OF CHOST) SHARE-OF-P)` - Get or set the SHARE-OF post ID of CHOST. +* `(NEW-MARKDOWN-BLOCK CLIENT CONTENT)` - Create a new MARKDOWN CONTENT-BLOCK, defining (part of) the text content of a post. + + **Parameters** + + * CLIENT - Initialized client object + * CONTENT - Markdown content for the new block. + + **Returns**: A MARKDOWN CONTENT-BLOCK object suitable for passing to NEW-POST or (SETF (CONTENT-BLOCKS ...) ...) + + **Accessors**: + + * `(CONTENT MARKDOWN-BLOCK)`/`(SETF (CONTENT MARKDOWN-BLOCK) NEW-CONTENT)` - Get or set the CONTENT of MARKDOWN-BLOCK. + +* `(NEW-FILE-ATTACHMENT CLIENT PATHNAME &KEY ALT-TEXT FILENAME CONTENT-TYPE)` - Create a new FILE-ATTACHMENT CONTENT-BLOCK, defining a file to upload as a post attachment. Currently, only images are supported. + + **Parameters** + + * CLIENT - Initialized client object + * PATHNAME - Pathname of the file to attach. + * ALT-TEXT - String specifying the alt-text for the attachment. Default NIL (no alt-text). + * FILENAME - String specifying the filename to upload the attachment as. If NIL, will use the actual filename (ie, if the pathname is #P"~/Pictures/foo.jpg", it will be uploaded as "foo.jpg". Default NIL. + * CONTENT-TYPE - String specifying the MIME type of the attachment (eg, "image/png" for a PNG file). If NIL, will use TRIVIAL-MIMES to derive the MIME type from the file specification. Default NIL. + + **Returns**: A FILE-ATTACHMENT CONTENT-BLOCK object suitable for passing to NEW-POST or (SETF (CONTENT-BLOCKS ...) ...) + + * `(ALT-TEXT FILE-ATTACHMENT)`/`(SETF (ALT-TEXT FILE-ATTACHMENT) NEW-ALT-TEXT)` - Get or set the ALT-TEXT of FILE-ATTACHMENT. + * `(ATTACHMENT-PATHNAME FILE-ATTACHMENT)`/`(SETF (ATTACHMENT-PATHNAME FILE-ATTACHMENT) NEW-ATTACHMENT-PATHNAME)` - Get or set the PATHNAME of FILE-ATTACHMENT. + * `(ATTACHMENT-FILENAME FILE-ATTACHMENT)`/`(SETF (ATTACHMENT-FILENAME FILE-ATTACHMENT) NEW-ATTACHMENT-FILENAME)` - Get or set the FILENAME of FILE-ATTACHMENT. + * `(ATTACHMENT-CONTENT-TYPE FILE-ATTACHMENT)`/`(SETF (ATTACHMENT-CONTENT-TYPE FILE-ATTACHMENT) NEW-ATTACHMENT-CONTENT-TYPE)` - Get or set the CONTENT-TYPE of FILE-ATTACHMENT. +* `(NEW-ATTACHMENT CLIENT ATTACHMENT-ID &KEY ALT-TEXT)` - Create a new ATTACHMENT CONTENT-BLOCK, defining a BLOCK related to an *existing* and already-uploaded attachment; this is typically not used on post creation but can be useful for editing existing posts. + + **Parameters** + + * CLIENT - Initialized client object + * ATTACHMENT-ID - The ID of the existing attachment. + * ALT-TEXT - String specifying the alt-text for the attachment. Default NIL (no alt-text). + + **Returns**: An ATTACHMENT CONTENT-BLOCK object suitable for passing to NEW-POST or (SETF (CONTENT-BLOCKS ...) ...) + + **Accessors**: + + * `(ATTACHMENT-ID ATTACHMENT)`/`(SETF (ATTACHMENT-ID ATTACHMENT) NEW-ATTACHMENT-ID)` - Get or set the ATTACHMENT-ID of ATTACHMENT. + * `(ALT-TEXT ATTACHMENT)`/`(SETF (ALT-TEXT ATTACHMENT) NEW-ALT-TEXT)` - Get or set the ALT-TEXT of ATTACHMENT. + +* (COPY-POST CLIENT POST) - Create a fresh copy of POST and all BLOCKs. + + **Parameters** + + * CLIENT - Initalized client object + * POST - An existing CHOST object. + + **Returns**: A freshly-consed copy of POST with freshly-consed copies of all BLOCKs. All lists associated with the newly-constructed object are guaranteed not to share structure with the equivalent lists associated with POST (so you can safely say, for instance, (SETF (TAGS NEW-CHOST) (CONS "This is a new tag" (TAGS NEW-CHOST))) without altering POST). + +* `(POST CLIENT POST)` - Post POST to Cohost. + + **Parameters** + + * CLIENT - Initalized client object + * POST - An existing CHOST object. + + **Returns**: `(VALUES POST POST-RESPONSE)` + + * NEW-POST - A new CHOST object, copied from POST and updated with the new post ID and with any attachments updated with their associated IDs as well. + * POST-RESPONSE - The parsed JSON response from the Cohost API endpoint. + + **Side Effects**: Posts POST to Cohost, with all post settings as specified in POST. + + **Notes**: This does all the heavy lifting for actually posting to Cohost. Does the following: + + * If POST has no ID set, calls the Cohost API to create a new post with the MARKDOWN and ATTACHMENT CONTENT-BLOCKs and configuration specified in POST. If an ID is set, instead updates the live post with that ID (assuming the logged-in user has rights to update that post). If DRAFT is set **or** if there are any FILE-ATTACHMENT BLOCKs in the CONTENT list, it creates or updates the post as draft. + * If there are any FILE-ATTACHMENT blocks, sequentially uploads each one, updates the draft post with the attachment IDs of the newly-created attachments, and collects new ATTACHMENT BLOCKs with the ID set to attach to NEW-POST object in place of the FILE-ATTACHMENTS assciated with POST. + * If there were attachments to upload and DRAFT is not set, updates the live post to unset draft state and post it to the live timeline. + * Creates and returns NEW-POST object with the content and settings from POST, the updated post ID returned by the Cohost API after posting (or the existing ID for post edits), and all FILE-ATTACHMENTs related to POST replaced with ATTACHMENT objects. Note that any BLOCKs other that FILE-ATTACHMENTs are the same objects as those associated with POST, and all other lists (like TAGS) share structure with POST's as well. + + Right now this is extremely brittle and does very little error checking so if something fails (image uploading for instance) it can leave the live post in a broken draft state. + +* `(SIMPLE-POST CLIENT PROJECT HEADLINE MARKDOWN &KEY DRAFT ADULT-CONTENT CONTENT-WARNINGS TAGS SHARE-OF)` - Create and post a simple (Markdown-only) post. + + **Parameters** + + * CLIENT - Logged-in client object + * PROJECT - String name of project to post as. Must be a project that the logged-in user has rights to post as. + * HEADLINE - A string specifying the post headline (or NIL or the empty string for no headline). + * MARKDOWN - A string specifying Markdown content for the post. + * DRAFT - If true, create the new post as a draft. Default NIL. + * ADULT-CONTENT - If true, marks the post as 18+. Default NIL. + * CONTENT-WARNINGS - A list of strings specifying content warnings to attach to the post. Default empty. + * TAGS - A list of strings specifying post tags. Default empty. + * SHARE-OF - The ID of an existing post. If set, the post will be created as a reply to the specificed post. Default NIL. + + **Returns**: A fully-hydrated CHOST object as defined by the parameters, with ID set as returned from the Cohost API. + + **Side Effects**: Posts the defined post to Cohost via the Cohost API, with all post settings as specified. + + **Notes**: This is a wrapper around NEW-POST, NEW-MARKDOWN-BLOCK and POST allowing a simple post with no attachments to be created and posted in one step. All the caveats of POST apply here. + +### Getters + +**WARNING**: All of these are very preliminary, rely on awkward hacks and do not yet return structured CLOS objects but just deserialized JSON as alists, as explained above. Everything below here can and will change at any time, in any way you can think of. **If you call any of these, anything can happen up to and including making your computer shoot out chains and demons like Hellraiser. Caveat utilitor!** + +* `(GET-POST CLIENT POST-ID)` - Get the content of a single post by ID + + **Parameters** + + * CLIENT - Logged-in client object + * POST-ID - ID of the post to fetch. + + **Returns**: Deserialized JSON representing the bare Cohost API response; all object keys are keywords translated from camel-case JS names as described in [the CL-JSON documentation](https://cl-json.common-lisp.dev/cl-json.html#CAMEL-CASE-TRANSLATION). + + **Notes**: As the Cohost API does not yet provide a convenient way to fetch a single post with just the ID, this is a two-step process that calls project\_post to get the associated project name and then calls tRPC posts.single\_post, so it requires two round-trips per post. + +* `(GET-DASH CLIENT)` - Get the current front page content for the logged-in user's default project + + **Parameters** + + * CLIENT - Logged-in client object + + **Returns**: Deserialized JSON representing the bare Cohost API response; all object keys are keywords translated from camel-case JS names as described in [the CL-JSON documentation](https://cl-json.common-lisp.dev/cl-json.html#CAMEL-CASE-TRANSLATION). + + **Notes**: There's no API call for this so it actually peels the JSON output out of a full page load of the cohost dashboard; expect a lot more data transfer than would be normal if this was just a JSON API response (around 60k by my tests). + +## FAQ -Licensed under the Strongest Public License. +* Shouldn't you use a proper license? + * No. +* Why isn't this on Github? + * Because I am [not part of your software supply chain](https://iliana.fyi/blog/software-supply-chain/) and Microsoft doesn't get to say that I am.