README.markdown (15998B)
1 # Cohost for Common Lisp 2 3 ### Decay <decay@todayiwilllaunchmyinfantsonintoorbit.com> 4 5 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: 6 7 **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.** 8 9 ## License 10 11 The STRONGEST PUBLIC LICENSE 12 13 ## Usage 14 15 Fetch this into your ASDF system directory (typically ~/common-lisp/): 16 17 ``` 18 $ cd ~/common-lisp 19 $ git clone https://todayiwilllaunchmyinfantsonintoorbit.com/cohost.git 20 ``` 21 22 Then in your favorite CL implementation, load it and its dependencies via Quicklisp: 23 24 ``` 25 CL-USER> (ql:quickload 'cohost) 26 ``` 27 28 ### REPL Examples 29 30 Load the library and create a client: 31 32 ``` 33 CL-USER> (ql:quickload 'cohost) 34 (COHOST) 35 CL-USER> (defvar *cohost* (cohost:init-client)) 36 *COHOST* 37 ``` 38 39 Login with standard user credentials: 40 41 ``` 42 CL-USER> (cohost:login *cohost* "decay@todayiwilllaunchmyinfantsonintoorbit.com" "hunter2") 43 "someopaquelogintoken" 44 ``` 45 46 Login with the token returned from an earlier LOGIN: 47 48 ``` 49 CL-USER> (cohost:login-with-token *cohost* "foo") 50 "someupdatedlogintoken" 51 ``` 52 53 Create a simple post: 54 55 ``` 56 CL-USER> (cohost:simple-post *cohost* "DecayWTF" "Simple Test Post" "This is a *simple* test post!" :tags '("lisp" "bot")) 57 #<COHOST.CLIENT::CHOST {100391EDA3}> 58 ``` 59 60 Create a CHOST object, construct it with both Markdown and an attachment and post it: 61 62 ``` 63 CL-USER> (let ((post (cohost:new-post *cohost* "DecayWTF" :headline "Test Post")) 64 (content (cohost:new-markdown-block *cohost* "This is the *markdown* content of my test post!")) 65 (pic (cohost:new-file-attachment *cohost* #p"~/Pictures/10f.gif"))) 66 (setf (cohost:content-blocks post) (list pic content)) 67 (cohost:post *cohost* post)) 68 #<COHOST.CLIENT::CHOST {1003646653}> 69 ``` 70 71 Get a post by ID: 72 73 ``` 74 CL-USER> (defvar *post-content* (cohost:get-post *cohost* 405148)) 75 *POST-CONTENT* 76 ``` 77 78 Get the current dashboard for the logged-in user's default project: 79 80 ``` 81 CL-USER> (defvar *posts* (cohost:get-dash *cohost*)) 82 *POSTS* 83 ``` 84 85 ## Public API 86 87 The full public API is exported from the COHOST package. Right now nothing is fully stable; in particular, 88 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. 89 90 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. 91 92 ### Setup/Teardown Functions 93 94 * `(INIT-CLIENT)` - Initializes the client 95 96 **Returns**: A client object to pass to other functions. 97 98 * `(LOGIN CLIENT EMAIL PASSWORD)` - Login with user credentials 99 100 **Parameters**: 101 102 * CLIENT - Initialized client object 103 * EMAIL - String email address 104 * PASSWORD - String password 105 106 **Returns**: On success, an opaque string authentication token that can be passed to LOGIN-WITH-TOKEN for later sessions. On failure, NIL. 107 108 **Side Effects**: Logs the CLIENT into Cohost. 109 110 **Notes**: Currently does not distinguish incorrect credentials from any other failure mode. If a token is returned, however, you can be sure that the login was successful and the client is ready for further calls. See LOGIN-WITH-TOKEN for details about the auth token and expirations. 111 112 * `(LOGIN-WITH-TOKEN CLIENT TOKEN)` - Login with authentication token 113 114 **Parameters**: 115 116 * CLIENT - Initialized client object 117 * TOKEN - An opaque authentication token returned from a previous call to LOGIN. 118 119 **Returns**: On success, an updated authentication token with refreshed expiration. On failure, NIL. 120 121 **Side Effects**: Logs the CLIENT into Cohost. 122 123 **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 will verify that the token was valid and that the client is correctly logged in; on success, it will return the updated auth token with a new expiration time, so this can also be used to refresh an active login (as can any other Cohost API call at present, since they will all implicitly update the auth cookie). 124 125 ### Post creation/manipulation 126 127 * `(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. 128 129 **Parameters** 130 131 * CLIENT - Initialized client object 132 * PROJECT - String name of project to post as. Must be a project that the logged-in user has rights to post as. 133 * 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. 134 * HEADLINE - A string specifying the post headline. Default NIL (no headline). 135 * DRAFT - If true, create the new post as a draft. Default NIL. 136 * ADULT-CONTENT - If true, marks the post as 18+. Default NIL. 137 * 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). 138 * CONTENT-WARNINGS - A list of strings specifying content warnings to attach to the post. Default empty. 139 * TAGS - A list of strings specifying post tags. Default empty. 140 * SHARE-OF - The ID of an existing post. If set, the post will be created as a reply to the specificed post. Default NIL. 141 142 **Returns**: A CHOST object as defined by the parameters. 143 144 **Accessors**: 145 146 * `(PROJECT CHOST)`/`(SETF (PROJECT CHOST) NEW-PROJECT)` - Get or set the PROJECT of CHOST. 147 * `(ID CHOST)`/`(SETF (ID CHOST) NEW-ID)` - Get or set the ID of CHOST. 148 * `(HEADLINE CHOST)`/`(SETF (HEADLINE CHOST) NEW-HEADLINE)` - Get or set the HEADLINE of CHOST. 149 * `(DRAFT CHOST)`/`(SETF (DRAFT CHOST) DRAFTP)` - Get or set the DRAFT state of CHOST. 150 * `(ADULT-CONTENT CHOST)`/`(SETF (ADULT-CONTENT CHOST) ADULT-CONTENT-P)` - Get or set the ADULT-CONTENT state of CHOST. 151 * `(CONTENT-BLOCKS CHOST)`/`(SETF (CONTENT-BLOCKS CHOST) NEW-BLOCKS)` - Get or set the CONTENT-BLOCKS list of CHOST. 152 * `(CONTENT-WARNINGS CHOST)`/`(SETF (CONTENT-WARNINGS CHOST) NEW-CWS)` - Get or set the CONTENT-WARNINGS list of CHOST. 153 * `(TAGS CHOST)`/`(SETF (TAGS CHOST) NEW-TAGS)` - Get or set the TAGS list of CHOST. 154 * `(SHARE-OF CHOST)`/`(SETF (SHARE-OF CHOST) SHARE-OF-P)` - Get or set the SHARE-OF post ID of CHOST. 155 * `(NEW-MARKDOWN-BLOCK CLIENT CONTENT)` - Create a new MARKDOWN CONTENT-BLOCK, defining (part of) the text content of a post. 156 157 **Parameters** 158 159 * CLIENT - Initialized client object 160 * CONTENT - Markdown content for the new block. 161 162 **Returns**: A MARKDOWN CONTENT-BLOCK object suitable for passing to NEW-POST or (SETF (CONTENT-BLOCKS ...) ...) 163 164 **Accessors**: 165 166 * `(CONTENT MARKDOWN-BLOCK)`/`(SETF (CONTENT MARKDOWN-BLOCK) NEW-CONTENT)` - Get or set the CONTENT of MARKDOWN-BLOCK. 167 168 * `(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. 169 170 **Parameters** 171 172 * CLIENT - Initialized client object 173 * PATHNAME - Pathname of the file to attach. 174 * ALT-TEXT - String specifying the alt-text for the attachment. Default NIL (no alt-text). 175 * 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. 176 * 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. 177 178 **Returns**: A FILE-ATTACHMENT CONTENT-BLOCK object suitable for passing to NEW-POST or (SETF (CONTENT-BLOCKS ...) ...) 179 180 * `(ALT-TEXT FILE-ATTACHMENT)`/`(SETF (ALT-TEXT FILE-ATTACHMENT) NEW-ALT-TEXT)` - Get or set the ALT-TEXT of FILE-ATTACHMENT. 181 * `(ATTACHMENT-PATHNAME FILE-ATTACHMENT)`/`(SETF (ATTACHMENT-PATHNAME FILE-ATTACHMENT) NEW-ATTACHMENT-PATHNAME)` - Get or set the PATHNAME of FILE-ATTACHMENT. 182 * `(ATTACHMENT-FILENAME FILE-ATTACHMENT)`/`(SETF (ATTACHMENT-FILENAME FILE-ATTACHMENT) NEW-ATTACHMENT-FILENAME)` - Get or set the FILENAME of FILE-ATTACHMENT. 183 * `(ATTACHMENT-CONTENT-TYPE FILE-ATTACHMENT)`/`(SETF (ATTACHMENT-CONTENT-TYPE FILE-ATTACHMENT) NEW-ATTACHMENT-CONTENT-TYPE)` - Get or set the CONTENT-TYPE of FILE-ATTACHMENT. 184 * `(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. 185 186 **Parameters** 187 188 * CLIENT - Initialized client object 189 * ATTACHMENT-ID - The ID of the existing attachment. 190 * ALT-TEXT - String specifying the alt-text for the attachment. Default NIL (no alt-text). 191 192 **Returns**: An ATTACHMENT CONTENT-BLOCK object suitable for passing to NEW-POST or (SETF (CONTENT-BLOCKS ...) ...) 193 194 **Accessors**: 195 196 * `(ATTACHMENT-ID ATTACHMENT)`/`(SETF (ATTACHMENT-ID ATTACHMENT) NEW-ATTACHMENT-ID)` - Get or set the ATTACHMENT-ID of ATTACHMENT. 197 * `(ALT-TEXT ATTACHMENT)`/`(SETF (ALT-TEXT ATTACHMENT) NEW-ALT-TEXT)` - Get or set the ALT-TEXT of ATTACHMENT. 198 199 * (COPY-POST CLIENT POST) - Create a fresh copy of POST and all BLOCKs. 200 201 **Parameters** 202 203 * CLIENT - Initalized client object 204 * POST - An existing CHOST object. 205 206 **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). 207 208 * `(POST CLIENT POST)` - Post POST to Cohost. 209 210 **Parameters** 211 212 * CLIENT - Initalized client object 213 * POST - An existing CHOST object. 214 215 **Returns**: `(VALUES NEW-POST POST-RESPONSE)` 216 217 * 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. 218 * POST-RESPONSE - The parsed JSON response from the Cohost API endpoint. 219 220 **Side Effects**: Posts POST to Cohost, with all post settings as specified in POST. 221 222 **Notes**: This does all the heavy lifting for actually posting to Cohost. Does the following: 223 224 * 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. 225 * 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. 226 * 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. 227 * 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. 228 229 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. 230 231 * `(SIMPLE-POST CLIENT PROJECT HEADLINE MARKDOWN &KEY DRAFT ADULT-CONTENT CONTENT-WARNINGS TAGS SHARE-OF)` - Create and post a simple (Markdown-only) post. 232 233 **Parameters** 234 235 * CLIENT - Logged-in client object 236 * PROJECT - String name of project to post as. Must be a project that the logged-in user has rights to post as. 237 * HEADLINE - A string specifying the post headline (or NIL or the empty string for no headline). 238 * MARKDOWN - A string specifying Markdown content for the post. 239 * DRAFT - If true, create the new post as a draft. Default NIL. 240 * ADULT-CONTENT - If true, marks the post as 18+. Default NIL. 241 * CONTENT-WARNINGS - A list of strings specifying content warnings to attach to the post. Default empty. 242 * TAGS - A list of strings specifying post tags. Default empty. 243 * SHARE-OF - The ID of an existing post. If set, the post will be created as a reply to the specificed post. Default NIL. 244 245 **Returns**: A fully-hydrated CHOST object as defined by the parameters, with ID set as returned from the Cohost API. 246 247 **Side Effects**: Posts the defined post to Cohost via the Cohost API, with all post settings as specified. 248 249 **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. 250 251 ### Getters 252 253 * `(GET-POST CLIENT POST-ID)` - Get the content of a single post by ID 254 255 **Parameters** 256 257 * CLIENT - Logged-in client object 258 * POST-ID - ID of the post to fetch. 259 260 **Returns**: A fully-hydrated CHOST object as described above. 261 262 **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. 263 264 * `(GET-DASH CLIENT)` - Get the current front page content for the logged-in user's default project 265 266 **Parameters** 267 268 * CLIENT - Logged-in client object 269 270 **Returns**: A list of fully-hydrated CHOST objects representing the posts present on the user's dash at the time of execution. 271 272 **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). 273 274 ### FAQ 275 276 * Shouldn't you use a proper license? 277 * No. 278 * Why isn't this on Github? 279 * 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.