|
1 | 1 | # neocities-deno
|
2 |
| -Neocities.org API client: written in TypeScript for use with Deno |
3 | 2 |
|
4 |
| -## To-do: |
| 3 | +[Neocities.org API](https://neocities.org/api) client: written in TypeScript for use with Deno |
5 | 4 |
|
6 |
| -- Review |
7 |
| -- Refactor |
8 |
| -- Document (see generated documentation at [doc.deno.land](https://doc.deno.land/https://deno.land/x/neocities/mod.ts)) |
| 5 | +> Generated type documentation is available at [doc.deno.land](https://doc.deno.land/https://deno.land/x/neocities/mod.ts). |
| 6 | +
|
| 7 | + |
| 8 | +## API keys |
| 9 | + |
| 10 | +Using this module requires an API key (also called a token) for your account. It's needed to access your account information via the API, so that you can do useful things in your script. |
| 11 | + |
| 12 | +> You can read more about tokens by navigating to your [account settings](https://neocities.org/settings), then select "Manage Site Settings", then select "API Key". |
| 13 | +
|
| 14 | +If you haven't already created a token for your account, you can do so by using the `get_token.ts` script. It will ask for your username and password, and then use the API to create a token for you. Once you have your token, you can use it with this module. Here's what it looks like to use the `get_token.ts` script: |
| 15 | + |
| 16 | +``` |
| 17 | +$ deno run https://deno.land/x/[email protected]/get_token.ts |
| 18 | +Provide your neocities username and password to obtain your API key: |
| 19 | +⚠️ ️Deno requests network access to "neocities.org". Allow? [y/n (y = yes allow, n = no deny)] y |
| 20 | +username jsejcksn |
| 21 | +password my_very_secret_actually_strong_password |
| 22 | +Your neocities API key (token) is: |
| 23 | +3287ea7b1960458d8fa1a33f73bf3eb5 |
| 24 | +``` |
| 25 | + |
| 26 | + |
| 27 | +## How to use |
| 28 | + |
| 29 | + |
| 30 | +### Setup and running in the console |
| 31 | + |
| 32 | +You can try the following examples. First, create a TypeScript module somewhere on your device: let's say you create an example module at `/neocities-example/example.ts`. This is how you can run your example with the [permissions ](https://deno.land/[email protected]/getting_started/permissions) that it needs: |
| 33 | + |
| 34 | +``` |
| 35 | +# cd to the directory where you created the TypeScript module |
| 36 | +$ cd /neocities-example |
| 37 | +
|
| 38 | +# Set any environment variables that you want to use |
| 39 | +$ export NEOCITIES_USERNAME=your_actual_username |
| 40 | +$ export NEOCITIES_PASSWORD=your_very_secret_actually_strong_password |
| 41 | +
|
| 42 | +# Including your API key (token) if you already have it: |
| 43 | +$ export NEOCITIES_TOKEN=your_actual_neocities_token |
| 44 | +
|
| 45 | +# Run the example module script with the permissions it needs |
| 46 | +$ deno run --allow-env=NEOCITIES_USERNAME,NEOCITIES_PASSWORD,NEOCITIES_TOKEN --allow-net=neocities.org --allow-read example.ts |
| 47 | +``` |
| 48 | + |
| 49 | + |
| 50 | +### Create an API client |
| 51 | + |
| 52 | +```ts |
| 53 | +import { assert} from 'https://deno.land/[email protected]/testing/asserts.ts'; |
| 54 | +import { NeocitiesAPI} from 'https://deno.land/x/[email protected]/mod.ts'; |
| 55 | + |
| 56 | +// Create an API client from a token |
| 57 | + |
| 58 | +const token = Deno.env.get('NEOCITIES_TOKEN'); // "3287ea7b1960458d8fa1a33f73bf3eb5" |
| 59 | +assert(token, 'Token not found'); |
| 60 | + |
| 61 | +const api = new NeocitiesAPI(token); |
| 62 | +``` |
| 63 | + |
| 64 | +```ts |
| 65 | +// If you haven't created/saved a token yet, that's ok: you can use your username + password |
| 66 | +// This example gets them from environment variables: don't store them in your source code 😅 |
| 67 | + |
| 68 | +const username = Deno.env.get('NEOCITIES_USERNAME'); // "jsejcksn" |
| 69 | +assert(username, 'Username not found'); |
| 70 | + |
| 71 | +const password = Deno.env.get('NEOCITIES_PASSWORD'); // "my_very_secret_actually_strong_password" |
| 72 | +assert(password, 'Password not found'); |
| 73 | + |
| 74 | +const api = await NeocitiesAPI.createFromCredentials(username, password); |
| 75 | +``` |
| 76 | + |
| 77 | + |
| 78 | +### Get site info |
| 79 | + |
| 80 | +```ts |
| 81 | +// Get info about your site |
| 82 | + |
| 83 | +const infoResult = await api.info(); |
| 84 | +console.log(infoResult); |
| 85 | +``` |
| 86 | + |
| 87 | +> ``` |
| 88 | +> { |
| 89 | +> result: "success", |
| 90 | +> info: { |
| 91 | +> sitename: "jsejcksn", |
| 92 | +> views: 9, |
| 93 | +> hits: 22, |
| 94 | +> created_at: 2022-01-28T13:11:09.000Z, |
| 95 | +> last_updated: 2022-01-31T00:04:03.000Z, |
| 96 | +> domain: null, |
| 97 | +> tags: [], |
| 98 | +> latest_ipfs_hash: null |
| 99 | +> } |
| 100 | +> } |
| 101 | +> ``` |
| 102 | +
|
| 103 | +```ts |
| 104 | +// Get info about the named site |
| 105 | +
|
| 106 | +const infoResult = await api.info('kyledrake'); |
| 107 | +console.log(infoResult); |
| 108 | +``` |
| 109 | +
|
| 110 | +> ``` |
| 111 | +> { |
| 112 | +> result: "success", |
| 113 | +> info: { |
| 114 | +> sitename: "kyledrake", |
| 115 | +> views: 874044, |
| 116 | +> hits: 1072518, |
| 117 | +> created_at: 2013-06-03T06:45:02.000Z, |
| 118 | +> last_updated: 2022-01-06T20:22:34.000Z, |
| 119 | +> domain: "kyledrake.com", |
| 120 | +> tags: [ "personal" ], |
| 121 | +> latest_ipfs_hash: "bafybeicmk3wkw5vqtdzaasybcfuhed3wqtqcfy7u5mfcwy7w2rk5lels4i" |
| 122 | +> } |
| 123 | +> } |
| 124 | +> ``` |
| 125 | +
|
| 126 | +
|
| 127 | +### List files |
| 128 | +
|
| 129 | +```ts |
| 130 | +// Get a list of all files for your site |
| 131 | +
|
| 132 | +const listResult = await api.list(); |
| 133 | +console.log(listResult); |
| 134 | +``` |
| 135 | +
|
| 136 | +> ``` |
| 137 | +> { |
| 138 | +> result: "success", |
| 139 | +> files: [ |
| 140 | +> { |
| 141 | +> path: "index.html", |
| 142 | +> is_directory: false, |
| 143 | +> size: 1083, |
| 144 | +> updated_at: 2022-01-28T13:11:09.000Z, |
| 145 | +> sha1_hash: "99bc354d84057d6900a71d0cb5e8dde069d8689e" |
| 146 | +> }, |
| 147 | +> { |
| 148 | +> path: "neocities.png", |
| 149 | +> is_directory: false, |
| 150 | +> size: 13232, |
| 151 | +> updated_at: 2022-01-28T13:11:09.000Z, |
| 152 | +> sha1_hash: "fd2ee41b1922a39a716cacb88c323d613b0955e4" |
| 153 | +> }, |
| 154 | +> { |
| 155 | +> path: "not_found.html", |
| 156 | +> is_directory: false, |
| 157 | +> size: 347, |
| 158 | +> updated_at: 2022-01-28T13:11:09.000Z, |
| 159 | +> sha1_hash: "d7f004e9d3b2eaaa8827f741356f1122dc9eb030" |
| 160 | +> }, |
| 161 | +> { |
| 162 | +> path: "style.css", |
| 163 | +> is_directory: false, |
| 164 | +> size: 298, |
| 165 | +> updated_at: 2022-01-28T13:11:09.000Z, |
| 166 | +> sha1_hash: "e516457acdb0d00710ab62cc257109ef67209ce8" |
| 167 | +> }, |
| 168 | +> { path: "test", is_directory: true, updated_at: 2022-01-31T22:14:11.000Z }, |
| 169 | +> { |
| 170 | +> path: "test/text_file1.txt", |
| 171 | +> is_directory: false, |
| 172 | +> size: 9, |
| 173 | +> updated_at: 2022-01-31T22:14:56.000Z, |
| 174 | +> sha1_hash: "02d92c580d4ede6c80a878bdd9f3142d8f757be8" |
| 175 | +> } |
| 176 | +> ] |
| 177 | +> } |
| 178 | +> ``` |
| 179 | +
|
| 180 | +```ts |
| 181 | +// Get a list of files in the provided directory path in your site |
| 182 | +
|
| 183 | +const listResult = await api.list('test'); |
| 184 | +console.log(listResult); |
| 185 | +``` |
| 186 | +
|
| 187 | +> ``` |
| 188 | +> { |
| 189 | +> result: "success", |
| 190 | +> files: [ |
| 191 | +> { |
| 192 | +> path: "test/text_file1.txt", |
| 193 | +> is_directory: false, |
| 194 | +> size: 9, |
| 195 | +> updated_at: 2022-01-31T22:14:56.000Z, |
| 196 | +> sha1_hash: null |
| 197 | +> } |
| 198 | +> ] |
| 199 | +> } |
| 200 | +> ``` |
| 201 | +
|
| 202 | +
|
| 203 | +### Upload files |
| 204 | +
|
| 205 | +> You can read more about [which file types can be uploaded](https://neocities.org/site_files/allowed_types). |
| 206 | +
|
| 207 | +Let's say you create a text file next to your example script, and you name it `hello-world.txt` with this as the contents: |
| 208 | +
|
| 209 | +``` |
| 210 | +hello world |
| 211 | + |
| 212 | +``` |
| 213 | +
|
| 214 | +You can upload it by referencing its local path on your device, and the path to where you want to upload it on your site. (You can do this for multiple files at once.) |
| 215 | +
|
| 216 | +```ts |
| 217 | +// Upload one or more files by local path |
| 218 | +
|
| 219 | +import {type UploadableFile} from 'https://deno.land/x/[email protected]/mod.ts'; |
| 220 | +
|
| 221 | +const files: UploadableFile[] = [ |
| 222 | + { |
| 223 | + localPath: './hello-world.txt', |
| 224 | + uploadPath: 'test/hello.txt', |
| 225 | + }, |
| 226 | +]; |
| 227 | +
|
| 228 | +const uploadResult = await api.upload(files); |
| 229 | +console.log(uploadResult); |
| 230 | +``` |
| 231 | + |
| 232 | +> ``` |
| 233 | +> { result: "success", message: "your file(s) have been successfully uploaded" } |
| 234 | +> ``` |
| 235 | +
|
| 236 | +You can also upload raw file data if you want to upload something that doesn't exist as a file on your device. Here's an example of uploading a text file that's created in the script: |
| 237 | +
|
| 238 | +```ts |
| 239 | +// Upload one or more files as raw data |
| 240 | +
|
| 241 | +import {type UploadableFile} from 'https://deno.land/x/[email protected]/mod.ts'; |
| 242 | +
|
| 243 | +const hello2FileData = `hello other planets, too!`; |
| 244 | +
|
| 245 | +const files: UploadableFile[] = [ |
| 246 | + { |
| 247 | + data: hello2FileData, |
| 248 | + uploadPath: 'test/hello2.txt', |
| 249 | + }, |
| 250 | +]; |
| 251 | +
|
| 252 | +const uploadResult = await api.upload(files); |
| 253 | +console.log(uploadResult); |
| 254 | +``` |
| 255 | +
|
| 256 | +> ``` |
| 257 | +> { result: "success", message: "your file(s) have been successfully uploaded" } |
| 258 | +> ``` |
| 259 | +
|
| 260 | +> You can even include a mix of both types of files in the files array: |
| 261 | +> - files on your device, using local paths |
| 262 | +> - files created from raw data |
| 263 | +
|
| 264 | +Even though the responses indicated that the uploads were successful, you can check to see the changes by using the `list` method from above with the `"test"` directory path that they were uploaded to: |
| 265 | +
|
| 266 | +```ts |
| 267 | +const listResult = await api.list('test'); |
| 268 | +console.log(listResult); |
| 269 | +``` |
| 270 | +
|
| 271 | +> ``` |
| 272 | +> { |
| 273 | +> result: "success", |
| 274 | +> files: [ |
| 275 | +> { |
| 276 | +> path: "test/hello.txt", |
| 277 | +> is_directory: false, |
| 278 | +> size: 12, |
| 279 | +> updated_at: 2022-01-31T22:44:53.000Z, |
| 280 | +> sha1_hash: null |
| 281 | +> }, |
| 282 | +> { |
| 283 | +> path: "test/hello2.txt", |
| 284 | +> is_directory: false, |
| 285 | +> size: 25, |
| 286 | +> updated_at: 2022-01-31T22:51:05.000Z, |
| 287 | +> sha1_hash: null |
| 288 | +> }, |
| 289 | +> { |
| 290 | +> path: "test/text_file1.txt", |
| 291 | +> is_directory: false, |
| 292 | +> size: 9, |
| 293 | +> updated_at: 2022-01-31T22:14:56.000Z, |
| 294 | +> sha1_hash: null |
| 295 | +> } |
| 296 | +> ] |
| 297 | +> } |
| 298 | +> ``` |
| 299 | +
|
| 300 | +
|
| 301 | +### Delete files |
| 302 | +
|
| 303 | +> **Be careful with this API method.** There is no way to undo a delete! |
| 304 | +
|
| 305 | +You can delete the two files that were uploaded in the last step: |
| 306 | +
|
| 307 | +```ts |
| 308 | +// Delete one or more paths (files/directories) |
| 309 | +
|
| 310 | +const filePathsToDelete = [ |
| 311 | + 'test/hello.txt', |
| 312 | + 'test/hello2.txt', |
| 313 | +]; |
| 314 | +
|
| 315 | +const deleteResult = await api.delete(filePathsToDelete); |
| 316 | +console.log(deleteResult); |
| 317 | +``` |
| 318 | +
|
| 319 | +> ``` |
| 320 | +> { result: "success", message: "file(s) have been deleted" } |
| 321 | +> ``` |
| 322 | +
|
| 323 | +Confirming that they are no longer in the `"test"` folder using the `list` method: |
| 324 | +
|
| 325 | +```ts |
| 326 | +console.log(await api.list('test')); |
| 327 | +``` |
| 328 | +
|
| 329 | +> ``` |
| 330 | +> { |
| 331 | +> result: "success", |
| 332 | +> files: [ |
| 333 | +> { |
| 334 | +> path: "test/text_file1.txt", |
| 335 | +> is_directory: false, |
| 336 | +> size: 9, |
| 337 | +> updated_at: 2022-01-31T22:14:56.000Z, |
| 338 | +> sha1_hash: null |
| 339 | +> } |
| 340 | +> ] |
| 341 | +> } |
| 342 | +> ``` |
| 343 | +
|
| 344 | +> You can also delete an entire directory at once, instead of specifying lots of files to delete. Again, be careful! |
| 345 | +
|
| 346 | +
|
| 347 | +## Functional programming |
| 348 | +
|
| 349 | +All of the examples above used an instance of the `NeocitiesAPI` stateful class, created from an API token which was used implicitly in each method call. |
| 350 | +
|
| 351 | +If you prefer a functional approach, you can use the functional form of each of the class methods, which are also exported: |
| 352 | +
|
| 353 | +> If you review the source code, you'll see that the class actually just uses these functions: |
| 354 | +
|
| 355 | +```ts |
| 356 | +import { |
| 357 | + deleteFiles, |
| 358 | + getSiteInfo, |
| 359 | + getToken, |
| 360 | + listFiles, |
| 361 | + uploadFiles, |
| 362 | + type UploadableFile, |
| 363 | +} from 'https://deno.land/x/[email protected]/mod.ts'; |
| 364 | +
|
| 365 | +// declare const username: string; |
| 366 | +// declare const password: string; |
| 367 | +
|
| 368 | +// get a token using credentials |
| 369 | +const token = await getToken(username, password); |
| 370 | +
|
| 371 | +// All of the functional forms accept a token as the first argument, |
| 372 | +// otherwise, they are identical to the class methods: |
| 373 | +
|
| 374 | +// info |
| 375 | +let infoResult = await getSiteInfo(token); |
| 376 | +infoResult = await getSiteInfo(token, 'kyledrake'); |
| 377 | +
|
| 378 | +// list |
| 379 | +let listResult = await listFiles(token); |
| 380 | +listResult = await listFiles(token, 'test'); |
| 381 | +
|
| 382 | +// upload |
| 383 | +const rawFileData = 'hello everyone'; |
| 384 | +const uploadPath = 'test/hello-everyone.txt'; |
| 385 | +const files: UploadableFile[] = [{data: rawFileData, uploadPath}]; |
| 386 | +const uploadResult = await uploadFiles(token, files); |
| 387 | +
|
| 388 | +// delete |
| 389 | +const filePathsToDelete = files.map(({uploadPath}) => uploadPath); |
| 390 | +const deleteResult = await deleteFiles(token, filePathsToDelete); |
| 391 | +``` |
0 commit comments