Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ To install the Nekosia.js module, use the following command:
npm install nekosia.js
```

Nekosia.js is an **ESM-first** package. Use `import` to load it in your project. For CommonJS environments simply `require('nekosia.js')` which loads the CJS build.

## 🔤 Tag list
You can find the main image categories [here](https://nekosia.cat/documentation?page=api-endpoints#tags-and-categories).
Expand All @@ -60,7 +61,7 @@ The full list of tags is available [on the Booru site](https://nekosia.cat/booru

### Simple Example
```js
const { NekosiaAPI } = require('nekosia.js');
import { NekosiaAPI } from 'nekosia.js';

(async () => {
const response1 = await NekosiaAPI.fetchCategoryImages('catgirl');
Expand All @@ -73,11 +74,21 @@ const { NekosiaAPI } = require('nekosia.js');
})();
```

### CommonJS Usage
```js
const { NekosiaAPI } = require('nekosia.js');

(async () => {
const img = await NekosiaAPI.fetchCategoryImages('catgirl');
console.log(img);
})();
```

### IP-based Sessions
In this example, we used an IP-based session. What does this mean? Thanks to this solution, a user with a specific IP address will not encounter duplicate images when selecting them randomly.

```js
const { NekosiaAPI } = require('nekosia.js');
import { NekosiaAPI } from 'nekosia.js';

(async () => {
const response = await NekosiaAPI.fetchCategoryImages('catgirl', {
Expand All @@ -95,7 +106,7 @@ const { NekosiaAPI } = require('nekosia.js');
You can also use `id`, but this requires providing a user identifier (e.g., from Discord). Pass this information in `id` as a string.

```js
const { NekosiaAPI } = require('nekosia.js');
import { NekosiaAPI } from 'nekosia.js';

(async () => {
const response = await NekosiaAPI.fetchCategoryImages('catgirl', {
Expand All @@ -116,7 +127,7 @@ https://github.com/Nekosia-API/nekosia.js/tree/main/examples

## Tags
```js
const { NekosiaAPI } = require('nekosia.js');
import { NekosiaAPI } from 'nekosia.js';

(async () => {
console.log(await NekosiaAPI.fetchTags()); // Simply returns all available tags, etc.
Expand All @@ -126,7 +137,7 @@ const { NekosiaAPI } = require('nekosia.js');

## Versions
```js
const { NekosiaVersion } = require('nekosia.js');
import { NekosiaVersion } from 'nekosia.js';

(async () => {
console.log(NekosiaVersion.module); // Returns the installed module version
Expand Down
2 changes: 1 addition & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,4 @@ export default [
},
ignores: ['node_modules', '*min.js', '*bundle*', 'build/*', 'dist/*'],
},
];
];
4 changes: 2 additions & 2 deletions examples/dynamicCategoryFetch.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { NekosiaAPI } = require('../index.js');
import { NekosiaAPI } from '../index.js';

const fetch = async (category, options = {}) => {
try {
Expand All @@ -13,4 +13,4 @@ const fetch = async (category, options = {}) => {
await fetch('catgirl');
await fetch('foxgirl', { session: 'id', id: 'user123', count: 2 });
await fetch('catgirl', { tags: 'animal-ears' });
})();
})();
4 changes: 2 additions & 2 deletions examples/fetchCategoryImages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { NekosiaAPI } = require('../index.js');
import { NekosiaAPI } from '../index.js';

(async () => {
const response = await NekosiaAPI.fetchCategoryImages('foxgirl', {
Expand All @@ -9,4 +9,4 @@ const { NekosiaAPI } = require('../index.js');
});

console.log(response);
})();
})();
4 changes: 2 additions & 2 deletions examples/fetchImages.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { NekosiaAPI } = require('../index.js');
import { NekosiaAPI } from '../index.js';

(async () => {
const response = await NekosiaAPI.fetchImages({
Expand All @@ -9,4 +9,4 @@ const { NekosiaAPI } = require('../index.js');
});

console.log(response);
})();
})();
4 changes: 2 additions & 2 deletions examples/fetchTags.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const { NekosiaAPI } = require('../index.js');
import { NekosiaAPI } from '../index.js';

(async () => {
const json = await NekosiaAPI.fetchTags();
console.log(json);
})();
})();
4 changes: 2 additions & 2 deletions examples/version.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
const { NekosiaVersion } = require('../index.js');
import { NekosiaVersion } from '../index.js';

(async () => {
console.log(`Nekosia.js: v${NekosiaVersion.module}`);

const data = await NekosiaVersion.api();
console.log(`API: v${data.version}`);
})();
})();
85 changes: 85 additions & 0 deletions index.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
const https = require('./services/https.cjs');
const { version: moduleVersion } = https;

const BASE_URL = 'https://api.nekosia.cat';
const API_URL = `${BASE_URL}/api/v1`;

class InternalNekosiaAPI {
buildQueryParams(options = {}) {
return Object.entries(options)
.filter(([, value]) =>
value != null &&
value !== '' &&
(!Array.isArray(value) || value.length > 0)
)
.map(([key, value]) => {
if (Array.isArray(value)) {
return `${encodeURIComponent(key)}=${value.map(v => encodeURIComponent(v)).join('%2C')}`;
}
if (typeof value === 'string' && value.includes(',')) {
throw new Error('String values must not contain commas. Use an array instead.');
}
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
})
.join('&');
}

async makeHttpRequest(endpoint) {
return https.get(endpoint);
}

async fetchCategoryImages(category, options = {}) {
if (typeof category !== 'string' || !category.trim()) {
throw new Error('Image category is required. For example: fetchCategoryImages(\'catgirl\')');
}

if (options.session && !['id', 'ip'].includes(options.session)) {
throw new Error('The `session` setting can contain only the following values `id` and `ip`, both as strings');
}

if (!options.session && options.id) {
throw new Error('`id` is not required if the session is `null` or `undefined`');
}

const query = this.buildQueryParams(options);
return this.makeHttpRequest(`${API_URL}/images/${encodeURIComponent(category)}${query ? `?${query}` : ''}`);
}

async fetchImages(options = {}) {
if (!Array.isArray(options.tags) || options.tags.length === 0) {
throw new Error('`tags` must be a non-empty array');
}

if (Array.isArray(options.blacklistedTags)) {
throw new Error('Unexpected `blacklistedTags` in `fetchImages()`, use `blacklist` instead');
}

return this.fetchCategoryImages('nothing', {
session: options.session,
id: options.id,
count: options.count,
additionalTags: options.tags,
blacklistedTags: options.blacklist,
});
}

async fetchTags() {
return this.makeHttpRequest(`${API_URL}/tags`);
}

async fetchById(id) {
if (typeof id !== 'string' || !id.trim()) throw new Error('`id` parameter is required');
return this.makeHttpRequest(`${API_URL}/getImageById/${id}`);
}
}

const NekosiaVersion = {
module: moduleVersion,
api: async () => https.get(BASE_URL),
};

const NekosiaAPI = new InternalNekosiaAPI();

const exportsObject = { NekosiaAPI, NekosiaVersion };
module.exports = exportsObject;
module.exports.default = exportsObject;
23 changes: 13 additions & 10 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const https = require('./services/https.js');
import https, { version as moduleVersion } from './services/https.js';

const BASE_URL = 'https://api.nekosia.cat';
const API_URL = `${BASE_URL}/api/v1`;

class NekosiaAPI {
class InternalNekosiaAPI {
buildQueryParams(options = {}) {
return Object.entries(options)
.filter(([, value]) =>
Expand All @@ -23,7 +24,7 @@ class NekosiaAPI {
}

async makeHttpRequest(endpoint) {
return https.get(endpoint);
return https.get(endpoint);
}

async fetchCategoryImages(category, options = {}) {
Expand Down Expand Up @@ -71,12 +72,14 @@ class NekosiaAPI {
}
}

const NekosiaVersion = {
module: https.version,
api: async () => https.get(BASE_URL),
export const NekosiaVersion = {
module: moduleVersion,
api: async () => https.get(BASE_URL),
};

module.exports = {
NekosiaAPI: new NekosiaAPI(),
NekosiaVersion,
};
export const NekosiaAPI = new InternalNekosiaAPI();

export default {
NekosiaAPI,
NekosiaVersion,
};
11 changes: 9 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,22 @@
},
"license": "MIT",
"author": "Sefinek <contact@nekosia.cat> (https://nekosia.cat)",
"type": "commonjs",
"type": "module",
"main": "index.js",
"exports": {
".": {
"import": "./index.js",
"require": "./index.cjs"
},
"./package.json": "./package.json"
},
"types": "types/index.d.ts",
"directories": {
"example": "examples",
"test": "test"
},
"scripts": {
"test": "jest",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"up": "ncu -u && npm install && npm update && npm audit fix"
},
"devDependencies": {
Expand Down
57 changes: 57 additions & 0 deletions services/https.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const https = require('https');
const { createRequire } = require('module');

const requirePkg = createRequire(__filename);
const { name, version: pkgVersion, devDependencies } = requirePkg('../package.json');
const version = pkgVersion;

const headers = {
'User-Agent': `${name}/${version} (+https://github.com/Nekosia-API/nekosia.js)${process.env.JEST_WORKER_ID ? ` jest/${devDependencies.jest.replace(/^[^0-9]*/, '')}` : ''}`,
'Accept': 'application/json',
'Content-Type': 'application/json',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
'DNT': '1',
};

const timeout = 15000;

const get = async url => {
if (!url || typeof url !== 'string') throw new Error('Missing URL, expected a string');

return new Promise((resolve, reject) => {
const req = https.get(url, { headers, timeout }, res => {
const { statusCode } = res;
if ((statusCode < 200 || statusCode >= 300) && statusCode !== 400) {
req.destroy();
return reject(new Error(`Unexpected HTTP Status Code: ${statusCode || 'unknown'}`));
}

let data = '';
res.on('data', chunk => data += chunk);
res.on('end', () => {
try {
resolve(JSON.parse(data));
} catch (err) {
reject(err);
}
});
});

req.on('error', err => {
req.destroy();
reject(err);
});

req.on('timeout', () => {
req.destroy();
reject(new Error(`Request timed out after ${timeout} ms`));
});
});
};

const httpService = { get, version };

module.exports = httpService;
module.exports.get = get;
module.exports.version = version;
13 changes: 10 additions & 3 deletions services/https.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const https = require('https');
const { name, version, devDependencies } = require('../package.json');
import https from 'https';
import { createRequire } from 'module';

const require = createRequire(import.meta.url);
const { name, version: pkgVersion, devDependencies } = require('../package.json');
const version = pkgVersion;

const headers = {
'User-Agent': `${name}/${version} (+https://github.com/Nekosia-API/nekosia.js)${process.env.JEST_WORKER_ID ? ` jest/${devDependencies.jest.replace(/^[^0-9]*/, '')}` : ''}`,
Expand Down Expand Up @@ -46,4 +50,7 @@ const get = async url => {
});
};

module.exports = { get, version };
const httpService = { get, version };

export { get, version };
export default httpService;
Loading