Skip to content

Commit d439eef

Browse files
authored
BREAKING refactor: finalize MVP (#1)
* refactor: move external dependency to deps.ts * refactor: extract dependency; rename getToken fn * feat: add interactive CLI module for getting token * refactor: improve consistency * refactor: move date parsing fn to utils * refactor: restructure modules * wip(docs): first draft of basic documentation * docs: add functional examples * docs: fix typo: missing args in functional example * docs: fix relative paths, improve grammar
1 parent c35a345 commit d439eef

File tree

8 files changed

+849
-343
lines changed

8 files changed

+849
-343
lines changed

README.md

Lines changed: 388 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,391 @@
11
# neocities-deno
2-
Neocities.org API client: written in TypeScript for use with Deno
32

4-
## To-do:
3+
[Neocities.org API](https://neocities.org/api) client: written in TypeScript for use with Deno
54

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+
```

deps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export {assert} from 'https://deno.land/[email protected]/testing/asserts.ts';
2+
export {DateTime} from 'https://unpkg.com/[email protected]/src/luxon.js';

0 commit comments

Comments
 (0)