diff --git a/README.md b/README.md index accc138..d1ea0f8 100644 --- a/README.md +++ b/README.md @@ -34,31 +34,52 @@ npm install koreanbots ### Koreanbots.MyBot -| 옵션 | 타입 | 필수 | 기본값 | 설명 | -|--------------------------|-------------|-----|------------|---------------------------------------------------------------------------| -| `token` | String | O | - | Koreanbots의 토큰 | -| `options.noWarning` | Boolean | | false | 모듈의 경고 알림을 끕니다 | -| `options.avoidRateLimit` | Boolean | | true | 레이트리밋을 최대한 피합니다 | -| `options.GCFlushOnMB` | Number | | 5 | 캐시 값 용량이 ${옵션의 숫자}MB가 될시 캐시를 초기화합니다 | -| `options.GCInterval` | Number(ms) | | 216000000 | `options.GCInterval` 밀리초마다 `options.GCFlushOnMB`MB를 넘을시 캐시를 초기화합니다 | +| 옵션 | 타입 | 필수 | 기본값 | 설명 | +|-----------------------------|-------------|-----|------------|----------------------------------------------------------------------------| +| `token` | String | O | - | Koreanbots의 토큰 | +| `options.noWarning` | Boolean | | false | 모듈의 경고 알림을 끕니다 | +| `options.avoidRateLimit` | Boolean | | true | 레이트리밋을 최대한 피합니다 | +| `options.autoFlush` | Number | | 100 | 캐시에 저장된 데이터 수가 `options.autoFlush`를 넘을시 캐시를 초기화합니다. (자동 캐시 관리) | +| `options.autoFlushInterval` | Number | | 3600000 | `options.autoFlushInterval`(밀리초)마다 캐시를 관리합니다 | ### Koreanbots.Bots -* 주의: Bots는 캐시를 자주 활용합니다. 이는 곧 메모리 사용량으로 직결되며 GC로 시작하는 옵션들을 잘 설정해주세요. +* 주의: Bots는 캐시를 자주 활용합니다. 이는 곧 메모리 사용량으로 직결되며 autoFlush로 시작하는 옵션들을 잘 설정해주세요. -| 옵션 | 타입 | 필수 | 기본값 | 설명 | -|--------------------------|-------------|-----|------------|---------------------------------------------------------------------------| -| `options.noWarning` | Boolean | | false | 모듈의 경고 알림을 끕니다 | -| `options.avoidRateLimit` | Boolean | | true | 레이트리밋을 최대한 피합니다 | -| `options.GCFlushOnMB` | Number | | 5 | 캐시 값 용량이 ${옵션의 숫자}MB가 될시 캐시를 초기화합니다 | -| `options.GCInterval` | Number(ms) | | 216000000 | `options.GCInterval` 밀리초마다 `options.GCFlushOnMB`MB를 넘을시 캐시를 초기화합니다 | +| 옵션 | 타입 | 필수 | 기본값 | 설명 | +|-----------------------------|-------------|-----|------------|----------------------------------------------------------------------------| +| `options.noWarning` | Boolean | | false | 모듈의 경고 알림을 끕니다 | +| `options.avoidRateLimit` | Boolean | | true | 레이트리밋을 최대한 피합니다 | +| `options.autoFlush` | Number | | 100 | 캐시에 저장된 데이터 수가 `options.autoFlush`를 넘을시 캐시를 초기화합니다. (자동 캐시 관리) | +| `options.autoFlushInterval` | Number | | 3600000 | `options.autoFlushInterval`(밀리초)마다 캐시를 관리합니다 | + +### Koreanbots.Widgets + +| 옵션 | 타입 | 필수 | 기본값 | 설명 | +|-----------------------------|-------------|-----|------------|----------------------------------------------------------------------------| +| `options.autoFlush` | Number | | 100 | 캐시에 저장된 데이터 수가 `options.autoFlush`를 넘을시 캐시를 초기화합니다. (자동 캐시 관리) | +| `options.autoFlushInterval` | Number | | 3600000 | `options.autoFlushInterval`(밀리초)마다 캐시를 관리합니다 | ## 수동 메모리(캐시) 관리 ```js const { SearchCache } = require("koreanbots")._cache.Bots -if(SearchCache.getStats().vsize >= 10 * 1024 * 1024) SearchCache.flushAll() +if(SearchCache.size >= 100) SearchCache.clear() +``` + +### 위젯 + +```js +const { Widgets } = require("koreanbots") +const widget = new Widgets() +const { MessageAttachment } = require("discord.js") + +widget.getVoteWidget(client.user.id, "jpeg").then(w => { + let wg = new MessageAttachment(w) + + message.channel.send(wg) +}).catch(er => { throw er }) ``` ## 테스트하기 @@ -125,7 +146,7 @@ client.login("토큰") - 아이디로 봇 정보 가져오기 (/bots/get/:id) ```js -const koreanbots = require('koreanbots'); +const koreanbots = require("koreanbots"); const Bots = new koreanbots.Bots() Bots.get("653534001742741552") diff --git a/package.json b/package.json index 8976bd8..e2c70e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "koreanbots", - "version": "1.1.4", + "version": "1.1.5", "description": "JS SDK for Koreanbots", "main": "src/index.js", "scripts": { @@ -27,9 +27,10 @@ }, "license": "MIT", "dependencies": { + "@discordjs/collection": "^0.1.6", "discord.js": "^12.2.0", - "node-cache": "^5.1.2", - "node-fetch": "^2.6.0" + "node-fetch": "^2.6.0", + "sharp": "^0.25.4" }, "devDependencies": { "eslint": "^7.1.0" diff --git a/src/bots.js b/src/bots.js index 87a89fc..33353ce 100644 --- a/src/bots.js +++ b/src/bots.js @@ -6,8 +6,8 @@ class Bots { this.options = options this.options.noWarning = options.noWarning === true this.options.avoidRateLimit = options.avoidRateLimit === undefined ? true : options.avoidRateLimit === true - this.options.GCFlushOnMB = options.GCFlushOnMB || 5 - this.options.GCInterval = options.GCInterval || 60000 * 60 * 60 + this.options.autoFlush = options.autoFlush || 100 + this.options.autoFlushInterval = options.autoFlushInterval || 60000 * 60 this.cache = BotsCache this.remainingPerEndpointCache = BotsdotjsRemainingPerEndpoint @@ -15,22 +15,18 @@ class Bots { search: SearchCache, category: CategoryCache } - setInterval(() => { - let cacheValueMB = this.cache.stats.vsize / 1024 / 1024 - let remainingPerEndpointCacheValueMB = this.remainingPerEndpointCache.stats.vsize / 1024 / 1024 - let searchValueMB = this.privateCache.search.stats.vsize / 1024 / 1024 - let categoryValueMB = this.privateCache.category.stats.vsize / 1024 / 1024 - - function flush(cache) { - cache.flushAll() - if (!this.options.noWarning) process.emitWarning("Koreanbots cache flushed by Koreanbots GC, because this cache exceeded 10MB of size.", "KoreanbotsGCWarning") - } - - if (cacheValueMB > this.options.GCFlushOnMB) flush(this.cache) - if (remainingPerEndpointCacheValueMB > this.options.GCFlushOnMB) flush(this.remainingPerEndpointCache) - if (searchValueMB > this.options.GCFlushOnMB) flush(this.privateCache.search) - if (categoryValueMB > this.options.GCFlushOnMB) flush(this.privateCache.category) - }, this.options.GCInterval) + if (this.options.autoFlushInterval && this.options.autoFlushInterval > 10000) { + setInterval(() => { + function flush(cache) { + if (cache.size >= this.options.autoFlush) cache.clear() + } + + [ + this.cache, this.remainingPerEndpointCache, + this._privateCache.search, this._privateCache.category + ].map(c => flush(c)) + }, this.options.autoFlushInterval) + } } /** @@ -74,7 +70,7 @@ class Bots { if (r.status === 429 || data === { size: 0, timeout: 0 }) { if (!this.options.noWarning) process.emitWarning(`Rate limited from ${r.url}`, "RateLimitWarning") - if (this.cache[endpoint] && opt.method !== "POST") return this.cache.get(endpoint) + if (this.cache.get(endpoint) && opt.method !== "POST") return this.cache.get(endpoint) return { code: 429, @@ -83,37 +79,23 @@ class Bots { } if (r.status === 200 && opt.disableGlobalCache !== true) { - if (this.cache.has(endpoint)) this.cache.del(endpoint) + if (this.cache.has(endpoint)) this.cache.delete(endpoint) data["updatedTimestamp"] = Date.now() - try { - this.cache.set(endpoint, data) - } catch (err) { - if (String(err).includes("ECACHEFULL")) { - this.cache.flushAll() - this.cache.set(endpoint, data) - } - } + this.cache.set(endpoint, data) } if (r.status === 200) { - if (this.remainingPerEndpointCache.has(endpoint)) this.remainingPerEndpointCache.del(endpoint) - - try { - this.remainingPerEndpointCache.set(endpoint, r.headers.get("x-ratelimit-remaining")) - } catch (err) { - if (String(err).includes("ECACHEFULL")) { - this.remainingPerEndpointCache.flushAll() - this.cache.set(endpoint, r.headers.get("x-ratelimit-remaining")) - } - } + if (this.remainingPerEndpointCache.has(endpoint)) this.remainingPerEndpointCache.delete(endpoint) + + this.remainingPerEndpointCache.set(endpoint, r.headers.get("x-ratelimit-remaining")) } if (r.status.toString().startsWith("4") || r.status.toString().startsWith("5")) throw new Error(data.message || JSON.stringify(data)) return data }) .catch(e => { - if (String(e).includes("body used already") && opt.method !== "POST") return this.cache[endpoint] + if (String(e).includes("body used already") && opt.method !== "POST") return this.cache.get(endpoint) throw e }) @@ -163,7 +145,7 @@ class Bots { }) setTimeout(() => { - if (this._privateCache.search.has(`${query}/${page}`)) this._privateCache.search.del(`${query}/${page}`) + if (this._privateCache.search.has(`${query}/${page}`)) this._privateCache.search.delete(`${query}/${page}`) }, 60000 * 30) this._privateCache.search.set(`${query}/${page}`, res) return res @@ -208,7 +190,7 @@ class Bots { }) setTimeout(() => { - if (this._privateCache.category.has(`${category}/${page}`)) this._privateCache.category.del(`${category}/${page}`) + if (this._privateCache.category.has(`${category}/${page}`)) this._privateCache.category.delete(`${category}/${page}`) }, 60000 * 30) this._privateCache.category.set(`${category}/${page}`, res) return res diff --git a/src/cache.js b/src/cache.js index e4497be..a776ac4 100644 --- a/src/cache.js +++ b/src/cache.js @@ -1,29 +1,12 @@ -const options = { - maxKeys: 10, - stdTTL: 60000 * 10 -} - -const NodeCache = require("node-cache") -const RemainingEndpointCache = new NodeCache(options) -const KoreanbotsCache = new NodeCache(options) - -const botsdotjsOptions = { - maxKeys: 10, - stdTTL: 60000 * 5 -} -const queryCacheOptions = { - maxKeys: 25, - stdTTL: 60000 * 5 -} - -const BotsCache = new NodeCache(botsdotjsOptions) -const BotsdotjsRemainingPerEndpoint = new NodeCache({ - maxKeys: 10, - stdTTL: 60000 * 60 -}) -const SearchCache = new NodeCache(queryCacheOptions) -const CategoryCache = new NodeCache(queryCacheOptions) +const { Collection } = require("@discordjs/collection") +const RemainingEndpointCache = new Collection() +const KoreanbotsCache = new Collection() +const BotsCache = new Collection() +const BotsdotjsRemainingPerEndpoint = new Collection() +const SearchCache = new Collection() +const CategoryCache = new Collection() +const WidgetsCache = new Collection() module.exports = { "index.js": { @@ -31,7 +14,8 @@ module.exports = { }, "bots.js": { BotsCache, BotsdotjsRemainingPerEndpoint, SearchCache, CategoryCache + }, + "widget.js": { + WidgetsCache } } - -//내가 봐도 이건 좀 개판이네 \ No newline at end of file diff --git a/src/index.js b/src/index.js index 2f66f03..e34938c 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ const req = require("node-fetch") const Bots = require("./bots") const KoreanbotsClient = require("./KoreanbotsClient") +const KoreanbotsWidgets = require("./widget") const { KoreanbotsCache, RemainingEndpointCache } = require("./cache")["index.js"] class MyBot { @@ -11,8 +12,8 @@ class MyBot { this.options = options this.options.noWarning = options.noWarning === true this.options.avoidRateLimit = options.avoidRateLimit === undefined ? true : options.avoidRateLimit === true - this.options.GCFlushOnMB = options.GCFlushOnMB || 5 - this.options.GCInterval = options.GCInterval || 60000 * 60 * 60 + this.options.autoFlush = options.autoFlush || 100 + this.options.autoFlushInterval = options.autoFlushInterval || 60000 * 60 this.updatedAt = null this.updatedTimestamp = null @@ -20,19 +21,15 @@ class MyBot { this.cache = KoreanbotsCache this.remainingPerEndpointCache = RemainingEndpointCache + if (this.options.autoFlushInterval && this.options.autoFlushInterval > 10000) { + setInterval(() => { + function flush(cache) { + if (cache.size >= this.options.autoFlush) cache.clear() + } - setInterval(() => { - let cacheValueMB = this.cache.stats.vsize / 1024 / 1024 - let remainingPerEndpointCacheValueMB = this.remainingPerEndpointCache.stats.vsize / 1024 / 1024 - - function flush(cache) { - cache.flushAll() - if(!this.options.noWarning) process.emitWarning("Koreanbots cache flushed by Koreanbots GC, because this cache exceeded 10MB of size.", "KoreanbotsGCWarning") - } - - if(cacheValueMB > this.options.GCFlushOnMB) flush(this.cache) - if (remainingPerEndpointCacheValueMB > this.options.GCFlushOnMB) flush(this.remainingPerEndpointCache) - }, this.options.GCInterval) + [this.cache, this.remainingPerEndpointCache].map(c => flush(c)) + }, this.options.autoFlushInterval) + } } /** @@ -90,7 +87,7 @@ class MyBot { } if (r.status === 200) { - if (this.cache.has(endpoint)) this.cache.del(endpoint) + if (this.cache.has(endpoint)) this.cache.delete(endpoint) data["updatedTimestamp"] = Date.now() @@ -98,22 +95,15 @@ class MyBot { this.cache.set(endpoint, data) } catch (err) { if (String(err).includes("ECACHEFULL")) { - this.cache.flushAll() + this.cache.clear() this.cache.set(endpoint, data) } } } if (r.status === 200) { - if (this.remainingPerEndpointCache.has(endpoint)) this.remainingPerEndpointCache.del(endpoint) + if (this.remainingPerEndpointCache.has(endpoint)) this.remainingPerEndpointCache.delete(endpoint) - try { - this.remainingPerEndpointCache.set(endpoint, r.headers.get("x-ratelimit-remaining")) - } catch (err) { - if(String(err).includes("ECACHEFULL")) { - this.remainingPerEndpointCache.flushAll() - this.cache.set(endpoint, r.headers.get("x-ratelimit-remaining")) - } - } + this.remainingPerEndpointCache.set(endpoint, r.headers.get("x-ratelimit-remaining")) } if (r.status.toString().startsWith("4") || r.status.toString().startsWith("5")) throw new Error(data.message || JSON.stringify(data)) @@ -161,7 +151,7 @@ class MyBot { async checkVote(id) { if (!id || typeof id !== "string") throw new Error("아이디가 주어지지 않았거나, 올바르지 않은 아이디입니다!") - if (this.cache[id]) return this.cache[id] + if (this.cache.get(id)) return this.cache.get(id) const res = await this._fetch(`/bots/voted/${id}`, { method: "GET", @@ -173,16 +163,18 @@ class MyBot { if (res.code !== 200) throw new Error(typeof res.message === "string" ? res.message : `올바르지 않은 응답이 반환되었습니다.\n응답: ${JSON.stringify(res)}`) setTimeout(() => { - if (this.cache[id]) delete this.cache[id] + if (this.cache.has(id)) this.cache.delete[id] }, 60000 * 60 * 6) - this.cache[id] = res + this.cache.set(id, res) return res } } let cache = require("./cache") -module.exports = { MyBot, Bots, KoreanbotsClient, _cache: { - MyBot: cache["index.js"], - Bots: cache["bots.js"] -}} \ No newline at end of file +module.exports = { + MyBot, Bots, KoreanbotsClient, _cache: { + MyBot: cache["index.js"], + Bots: cache["bots.js"] + }, Widgets: KoreanbotsWidgets +} diff --git a/src/widget.js b/src/widget.js new file mode 100644 index 0000000..b84696a --- /dev/null +++ b/src/widget.js @@ -0,0 +1,57 @@ +const { WidgetsCache } = require("./cache")["widget.js"] +const req = require("node-fetch") +const sharp = require("sharp") + +class KoreanbotsWidgets { + constructor(options) { + this.options = options + this.options.autoFlush = options.autoFlush || 100 + this.options.autoFlushInterval = options.autoFlushInterval || 60000 * 60 + + this.cache = WidgetsCache + + this.allowedFormats = ["jpeg", "png", "webp", "svg"] + + if (this.options.autoFlushInterval && this.options.autoFlushInterval > 10000) { + setInterval(() => { + function flush(cache) { + if (cache.size >= this.options.autoFlush) cache.clear() + } + + [this.cache, this.remainingPerEndpointCache].map(c => flush(c)) + }, this.options.autoFlushInterval) + } + } + + async _mkWidget(type, id, format) { + if (!this.allowedFormats(format)) throw new Error(`해당 포맷은 지원되지 않습니다. 지원되는 포맷: ${this.allowedFormats.join(", ")}`) + if (this.cache.get(`${id}/${format}`)) return this.cache.get(`${id}/${format}`) + + const res = await req(this[`get${type}WidgetURL`](id)).then(r => r.buffer()) + + if (format === "svg") var widget = res + else var widget = sharp(res)[format]() //eslint-disable-line no-redeclare + + this.cache.set(`${id}/${format}`, widget) + + return widget + } + + getVoteWidgetURL(id) { + return `https://api.koreanbots.dev/widget/bots/votes/${id}.svg` + } + + async getVoteWidget(id, format = "png") { + return this._mkWidget("Vote", id, format) + } + + getServerWidgetURL(id) { + return `https://api.koreanbots.dev/widget/bots/servers/${id}.svg` + } + + async getServerWidget(id, format = "png") { + return this._mkWidget("Server", id, format) + } +} + +module.exports = KoreanbotsWidgets \ No newline at end of file