@k4la/library 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,219 @@
1
+ /**
2
+ * @file lib/utils/blackjack.js
3
+ * @description Blackjack game utilities
4
+ * @author K4lameety
5
+ */
6
+
7
+ const jimp = require('jimp')
8
+
9
+ const CARDS_URL = "https://raw.githubusercontent.com/hayeah/playing-cards-assets/master/png/"
10
+
11
+ const gameData = {}
12
+
13
+ const createSession = (id) => {
14
+ gameData[id] = {
15
+ id: id,
16
+ status: 'waiting',
17
+ deck: [],
18
+ dealer: [],
19
+ player: [],
20
+ winner: null
21
+ }
22
+ return gameData[id]
23
+ }
24
+
25
+ const getSession = (id) => gameData[id] || false
26
+ const deleteSession = (id) => delete gameData[id]
27
+
28
+ const generateDeck = () => {
29
+ const suits = ['clubs', 'diamonds', 'hearts', 'spades']
30
+ const values = ['ace', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'jack', 'queen', 'king']
31
+ let deck = []
32
+
33
+ for (let suit of suits) {
34
+ for (let val of values) {
35
+ let score = parseInt(val)
36
+ if (val === 'ace') score = 11
37
+ if (['jack', 'queen', 'king'].includes(val)) score = 10
38
+
39
+ let filename = `${val}_of_${suit}.png`
40
+
41
+ deck.push({
42
+ id: filename,
43
+ value: val,
44
+ suit: suit,
45
+ score: score
46
+ })
47
+ }
48
+ }
49
+ for (let i = deck.length - 1; i > 0; i--) {
50
+ const j = Math.floor(Math.random() * (i + 1));
51
+ [deck[i], deck[j]] = [deck[j], deck[i]]
52
+ }
53
+ return deck
54
+ }
55
+
56
+ const calculateScore = (hand) => {
57
+ let score = 0
58
+ let aceCount = 0
59
+
60
+ for (let card of hand) {
61
+ score += card.score
62
+ if (card.value === 'ace') aceCount++
63
+ }
64
+
65
+ while (score > 21 && aceCount > 0) {
66
+ score -= 10
67
+ aceCount--
68
+ }
69
+ return score
70
+ }
71
+
72
+ const startGame = (id) => {
73
+ let session = getSession(id)
74
+ if (!session) session = createSession(id)
75
+
76
+ session.status = 'playing'
77
+ session.deck = generateDeck()
78
+ session.player = []
79
+ session.dealer = []
80
+
81
+ session.player.push(session.deck.pop())
82
+ session.dealer.push(session.deck.pop())
83
+ session.player.push(session.deck.pop())
84
+ session.dealer.push(session.deck.pop())
85
+
86
+ let pScore = calculateScore(session.player)
87
+ if (pScore === 21) {
88
+ session.status = 'end'
89
+ session.winner = 'player_blackjack'
90
+ }
91
+
92
+ return session
93
+ }
94
+
95
+ const hit = (id) => {
96
+ let session = getSession(id)
97
+ if (!session || session.status !== 'playing') return { status: 'error' }
98
+
99
+ let card = session.deck.pop()
100
+ session.player.push(card)
101
+
102
+ let score = calculateScore(session.player)
103
+ if (score > 21) {
104
+ session.status = 'end'
105
+ return { status: 'bust', card, score }
106
+ }
107
+
108
+ return { status: 'ok', card, score }
109
+ }
110
+
111
+ const stand = (id) => {
112
+ let session = getSession(id)
113
+ if (!session || session.status !== 'playing') return { status: 'error' }
114
+
115
+ let dealerScore = calculateScore(session.dealer)
116
+
117
+ while (dealerScore < 17) {
118
+ session.dealer.push(session.deck.pop())
119
+ dealerScore = calculateScore(session.dealer)
120
+ }
121
+
122
+ let playerScore = calculateScore(session.player)
123
+ session.status = 'end'
124
+
125
+ let result = ''
126
+ if (dealerScore > 21) result = 'dealer_bust'
127
+ else if (playerScore > dealerScore) result = 'win'
128
+ else if (playerScore < dealerScore) result = 'lose'
129
+ else result = 'push'
130
+
131
+ return { status: result, dealerScore, playerScore }
132
+ }
133
+
134
+ const canvas = async (id) => {
135
+ let session = getSession(id)
136
+ if (!session) return null
137
+
138
+ let bg = new jimp(800, 600, 0x35654dFF)
139
+ let font = await jimp.loadFont(jimp.FONT_SANS_32_WHITE)
140
+ let fontBig = await jimp.loadFont(jimp.FONT_SANS_64_WHITE)
141
+
142
+ let cardBack = new jimp(120, 175, 0x800000FF)
143
+ let border = new jimp(110, 165, 0xFFFFFF22)
144
+ cardBack.composite(border, 5, 5)
145
+
146
+ let dx = 50
147
+ let dy = 50
148
+ let dealerScore = 0
149
+
150
+ for (let i = 0; i < session.dealer.length; i++) {
151
+ if (session.status === 'playing' && i === 1) {
152
+ bg.composite(cardBack, dx, dy)
153
+ } else {
154
+ try {
155
+ let url = CARDS_URL + session.dealer[i].id
156
+ let img = await jimp.read(url)
157
+ img.resize(120, 175)
158
+ bg.composite(img, dx, dy)
159
+ } catch (e) {
160
+ bg.print(font, dx, dy, session.dealer[i].value)
161
+ }
162
+ }
163
+ dx += 130
164
+ }
165
+
166
+ if (session.status === 'playing') {
167
+ bg.print(font, 50, 240, `Dealer: ${session.dealer[0].score} + ?`)
168
+ } else {
169
+ dealerScore = calculateScore(session.dealer)
170
+ bg.print(font, 50, 240, `Dealer: ${dealerScore}`)
171
+ }
172
+
173
+ let px = 50
174
+ let py = 350
175
+ for (let card of session.player) {
176
+ try {
177
+ let url = CARDS_URL + card.id
178
+ let img = await jimp.read(url)
179
+ img.resize(120, 175)
180
+ bg.composite(img, px, py)
181
+ } catch (e) {
182
+ bg.print(font, px, py, card.value)
183
+ }
184
+ px += 130
185
+ }
186
+
187
+ let playerScore = calculateScore(session.player)
188
+ bg.print(font, 50, 540, `Player: ${playerScore}`)
189
+
190
+ if (session.status === 'end') {
191
+ let text = ""
192
+ let color = 0
193
+
194
+ if (session.winner === 'player_blackjack') text = "BLACKJACK!"
195
+ else if (playerScore > 21) text = "BUSTED!"
196
+ else if (dealerScore > 21) text = "DEALER BUST!"
197
+ else if (playerScore > dealerScore) text = "YOU WIN!"
198
+ else if (playerScore < dealerScore) text = "YOU LOSE!"
199
+ else text = "PUSH"
200
+
201
+ let overlay = new jimp(800, 150, 0x000000AA)
202
+ bg.composite(overlay, 0, 225)
203
+
204
+ let textWidth = jimp.measureText(fontBig, text)
205
+ bg.print(fontBig, 400 - (textWidth / 2), 265, text)
206
+ }
207
+
208
+ return await bg.getBufferAsync(jimp.MIME_JPEG)
209
+ }
210
+
211
+ module.exports = {
212
+ createSession,
213
+ startGame,
214
+ hit,
215
+ stand,
216
+ getSession,
217
+ deleteSession,
218
+ canvas
219
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @file lib/converter.js
3
+ * @description Media conversion utilities using ffmpeg
4
+ * @author K4lameety
5
+ */
6
+
7
+ const fs = require('fs')
8
+ const path = require('path')
9
+ const { spawn } = require('child_process')
10
+ const os = require('os')
11
+
12
+ let ffmpegPath;
13
+ try {
14
+ ffmpegPath = require('@ffmpeg-installer/ffmpeg').path;
15
+ } catch (e) {
16
+ ffmpegPath = 'ffmpeg';
17
+ }
18
+
19
+ function ffmpeg(buffer, args = [], ext = '', ext2 = '') {
20
+ return new Promise(async (resolve, reject) => {
21
+ try {
22
+ let tmp = path.join(os.tmpdir(), Date.now() + '.' + ext)
23
+ let out = tmp + '.' + ext2
24
+
25
+ await fs.promises.writeFile(tmp, buffer)
26
+
27
+ spawn(ffmpegPath, [
28
+ '-y',
29
+ '-i', tmp,
30
+ ...args,
31
+ out
32
+ ])
33
+ .on('error', reject)
34
+ .on('close', async (code) => {
35
+ try {
36
+ await fs.promises.unlink(tmp)
37
+ if (code !== 0) return reject(code)
38
+
39
+ const result = await fs.promises.readFile(out)
40
+
41
+ await fs.promises.unlink(out)
42
+
43
+ resolve(result)
44
+ } catch (e) {
45
+ reject(e)
46
+ }
47
+ })
48
+ } catch (e) {
49
+ reject(e)
50
+ }
51
+ })
52
+ }
53
+
54
+ /**
55
+ * Convert Audio/Video to MP3
56
+ * @param {Buffer} buffer
57
+ * @param {String} ext
58
+ */
59
+ function toAudio(buffer, ext) {
60
+ return ffmpeg(buffer, [
61
+ '-vn',
62
+ '-ac', '2',
63
+ '-b:a', '128k',
64
+ '-ar', '44100',
65
+ '-f', 'mp3'
66
+ ], ext, 'mp3')
67
+ }
68
+
69
+ /**
70
+ * Convert Audio to Voice Note (Opus/PTT)
71
+ * @param {Buffer} buffer
72
+ * @param {String} ext
73
+ */
74
+ function toPTT(buffer, ext) {
75
+ return ffmpeg(buffer, [
76
+ '-vn',
77
+ '-c:a', 'libopus',
78
+ '-b:a', '128k',
79
+ '-vbr', 'on',
80
+ '-compression_level', '10'
81
+ ], ext, 'opus')
82
+ }
83
+
84
+ /**
85
+ * Convert Video to MP4
86
+ * @param {Buffer} buffer
87
+ * @param {String} ext
88
+ */
89
+ function toVideo(buffer, ext) {
90
+ return ffmpeg(buffer, [
91
+ '-c:v', 'libx264',
92
+ '-c:a', 'aac',
93
+ '-ab', '128k',
94
+ '-ar', '44100',
95
+ '-crf', '32',
96
+ '-preset', 'slow'
97
+ ], ext, 'mp4')
98
+ }
99
+
100
+ module.exports = {
101
+ toAudio,
102
+ toPTT,
103
+ toVideo,
104
+ ffmpeg,
105
+ }
package/lib/exif.js ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @file lib/exif.js
3
+ * @description Functions to handle EXIF metadata for WebP images and stickers
4
+ * @author K4lameety
5
+ */
6
+
7
+ const fs = require('fs')
8
+ const { tmpdir } = require("os")
9
+ const Crypto = require("crypto")
10
+ const ffmpegPath = require('@ffmpeg-installer/ffmpeg').path
11
+ const ff = require('fluent-ffmpeg')
12
+ const webp = require("node-webpmux")
13
+ const path = require("path")
14
+
15
+ ff.setFfmpegPath(ffmpegPath)
16
+
17
+ async function imageToWebp(media) {
18
+ const tmpFileOut = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
19
+ const tmpFileIn = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.jpg`)
20
+
21
+ fs.writeFileSync(tmpFileIn, media)
22
+
23
+ await new Promise((resolve, reject) => {
24
+ ff(tmpFileIn)
25
+ .on("error", reject)
26
+ .on("end", () => resolve(true))
27
+ .addOutputOptions([
28
+ "-vcodec",
29
+ "libwebp",
30
+ "-vf",
31
+ "scale='min(320,iw)':min'(320,ih)':force_original_aspect_ratio=decrease,fps=15, pad=320:320:-1:-1:color=white@0.0, split [a][b]; [a] palettegen=reserve_transparent=on:transparency_color=ffffff [p]; [b][p] paletteuse"
32
+ ])
33
+ .toFormat("webp")
34
+ .save(tmpFileOut)
35
+ })
36
+
37
+ const buff = fs.readFileSync(tmpFileOut)
38
+ fs.unlinkSync(tmpFileOut)
39
+ fs.unlinkSync(tmpFileIn)
40
+ return buff
41
+ }
42
+
43
+ async function videoToWebp(media) {
44
+ const tmpFileOut = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
45
+ const tmpFileIn = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.mp4`)
46
+
47
+ fs.writeFileSync(tmpFileIn, media)
48
+
49
+ await new Promise((resolve, reject) => {
50
+ ff(tmpFileIn)
51
+ .on("error", reject)
52
+ .on("end", () => resolve(true))
53
+ .addOutputOptions([
54
+ "-vcodec",
55
+ "libwebp",
56
+ "-vf",
57
+ "scale='min(320,iw)':min'(320,ih)':force_original_aspect_ratio=decrease,fps=15, pad=320:320:-1:-1:color=white@0.0, split [a][b]; [a] palettegen=reserve_transparent=on:transparency_color=ffffff [p]; [b][p] paletteuse",
58
+ "-loop",
59
+ "0",
60
+ "-ss",
61
+ "00:00:00",
62
+ "-t",
63
+ "00:00:05",
64
+ "-preset",
65
+ "default",
66
+ "-an",
67
+ "-vsync",
68
+ "0"
69
+ ])
70
+ .toFormat("webp")
71
+ .save(tmpFileOut)
72
+ })
73
+
74
+ const buff = fs.readFileSync(tmpFileOut)
75
+ fs.unlinkSync(tmpFileOut)
76
+ fs.unlinkSync(tmpFileIn)
77
+ return buff
78
+ }
79
+
80
+ async function writeExifImg(media, metadata) {
81
+ let wMedia = await imageToWebp(media)
82
+ const tmpFileIn = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
83
+ const tmpFileOut = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
84
+ fs.writeFileSync(tmpFileIn, wMedia)
85
+
86
+ if (metadata.packname || metadata.author) {
87
+ const img = new webp.Image()
88
+ const json = {
89
+ "sticker-pack-id": metadata.packId || `com.k4lameety.sticker`, // ID Default
90
+ "sticker-pack-name": metadata.packname,
91
+ "sticker-pack-publisher": metadata.author,
92
+ "emojis": metadata.categories ? metadata.categories : [""]
93
+ }
94
+ const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00])
95
+ const jsonBuff = Buffer.from(JSON.stringify(json), "utf-8")
96
+ const exif = Buffer.concat([exifAttr, jsonBuff])
97
+ exif.writeUIntLE(jsonBuff.length, 14, 4)
98
+ await img.load(tmpFileIn)
99
+ fs.unlinkSync(tmpFileIn)
100
+ img.exif = exif
101
+ await img.save(tmpFileOut)
102
+ return tmpFileOut
103
+ }
104
+ }
105
+
106
+ async function writeExifVid(media, metadata) {
107
+ let wMedia = await videoToWebp(media)
108
+ const tmpFileIn = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
109
+ const tmpFileOut = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
110
+ fs.writeFileSync(tmpFileIn, wMedia)
111
+
112
+ if (metadata.packname || metadata.author) {
113
+ const img = new webp.Image()
114
+ const json = {
115
+ "sticker-pack-id": metadata.packId || `com.k4lameety.sticker`,
116
+ "sticker-pack-name": metadata.packname,
117
+ "sticker-pack-publisher": metadata.author,
118
+ "emojis": metadata.categories ? metadata.categories : [""]
119
+ }
120
+ const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00])
121
+ const jsonBuff = Buffer.from(JSON.stringify(json), "utf-8")
122
+ const exif = Buffer.concat([exifAttr, jsonBuff])
123
+ exif.writeUIntLE(jsonBuff.length, 14, 4)
124
+ await img.load(tmpFileIn)
125
+ fs.unlinkSync(tmpFileIn)
126
+ img.exif = exif
127
+ await img.save(tmpFileOut)
128
+ return tmpFileOut
129
+ }
130
+ }
131
+
132
+ async function writeExif(media, metadata) {
133
+ let wMedia = /webp/.test(media.mimetype) ? media.data : /image/.test(media.mimetype) ? await imageToWebp(media.data) : /video/.test(media.mimetype) ? await videoToWebp(media.data) : ""
134
+ const tmpFileIn = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
135
+ const tmpFileOut = path.join(tmpdir(), `${Crypto.randomBytes(6).readUIntLE(0, 6).toString(36)}.webp`)
136
+ fs.writeFileSync(tmpFileIn, wMedia)
137
+
138
+ if (metadata.packname || metadata.author) {
139
+ const img = new webp.Image()
140
+ const json = {
141
+ "sticker-pack-id": metadata.packId || `com.k4lameety.sticker`,
142
+ "sticker-pack-name": metadata.packname,
143
+ "sticker-pack-publisher": metadata.author,
144
+ "emojis": metadata.categories ? metadata.categories : [""]
145
+ }
146
+ const exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00])
147
+ const jsonBuff = Buffer.from(JSON.stringify(json), "utf-8")
148
+ const exif = Buffer.concat([exifAttr, jsonBuff])
149
+ exif.writeUIntLE(jsonBuff.length, 14, 4)
150
+ await img.load(tmpFileIn)
151
+ fs.unlinkSync(tmpFileIn)
152
+ img.exif = exif
153
+ await img.save(tmpFileOut)
154
+ return tmpFileOut
155
+ }
156
+ }
157
+
158
+ async function exifAvatar(buffer, packname, author, categories = [''], extra = {}) {
159
+ const img = new webp.Image() // Menggunakan require di atas, bukan import dinamis
160
+ const stickerPackId = extra.packId || Crypto.randomBytes(32).toString('hex')
161
+ const json = {
162
+ 'sticker-pack-id': stickerPackId,
163
+ 'sticker-pack-name': packname,
164
+ 'sticker-pack-publisher': author,
165
+ 'emojis': categories,
166
+ 'is-avatar-sticker': 1,
167
+ ...extra
168
+ }
169
+ let exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00])
170
+ let jsonBuffer = Buffer.from(JSON.stringify(json), 'utf8')
171
+ let exif = Buffer.concat([exifAttr, jsonBuffer])
172
+ exif.writeUIntLE(jsonBuffer.length, 14, 4)
173
+ await img.load(buffer)
174
+ img.exif = exif
175
+ return await img.save(null)
176
+ }
177
+
178
+ async function addExif(webpSticker, packname, author, categories = [''], extra = {}) {
179
+ const img = new webp.Image()
180
+ const stickerPackId = extra.packId || Crypto.randomBytes(32).toString('hex')
181
+ const json = {
182
+ 'sticker-pack-id': stickerPackId,
183
+ 'sticker-pack-name': packname,
184
+ 'sticker-pack-publisher': author,
185
+ 'emojis': categories,
186
+ ...extra
187
+ }
188
+ let exifAttr = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00])
189
+ let jsonBuffer = Buffer.from(JSON.stringify(json), 'utf8')
190
+ let exif = Buffer.concat([exifAttr, jsonBuffer])
191
+ exif.writeUIntLE(jsonBuffer.length, 14, 4)
192
+ await img.load(webpSticker)
193
+ img.exif = exif
194
+ return await img.save(null)
195
+ }
196
+
197
+ module.exports = {
198
+ imageToWebp,
199
+ videoToWebp,
200
+ writeExifImg,
201
+ writeExifVid,
202
+ writeExif,
203
+ exifAvatar,
204
+ addExif
205
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * @file lib/function.js
3
+ * @description General utility functions
4
+ * @author K4lameety
5
+ */
6
+
7
+ function formatRupiah(angka) {
8
+ return "Rp " + angka.toLocaleString('id-ID');
9
+ }
10
+
11
+ module.exports = {
12
+ formatRupiah
13
+ };
package/lib/game.js ADDED
@@ -0,0 +1,58 @@
1
+ /**
2
+ * @file lib/game.js
3
+ * @description Game database handler
4
+ * @author K4lameety
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ const pickRandom = (arr) => {
11
+ return arr[Math.floor(Math.random() * arr.length)];
12
+ };
13
+
14
+ const asahOtakDb = require('../data/asahotak.json');
15
+ const susunKataDb = require('../data/susunkata.json');
16
+ const tebakBenderaDb = require('../data/tebakbendera.json');
17
+ const tebakKimiaDb = require('../data/tebakkimia.json');
18
+ const tebakKataDb = require('../data/tebakkata.json');
19
+ const tebakGambarDb = require('../data/tebakgambar.json');
20
+ const family100Db = require('../data/family100.json');
21
+
22
+ function asahOtak() {
23
+ return pickRandom(asahOtakDb);
24
+ }
25
+
26
+ function susunKata() {
27
+ return pickRandom(susunKataDb);
28
+ }
29
+
30
+ function tebakBendera() {
31
+ return pickRandom(tebakBenderaDb);
32
+ }
33
+
34
+ function tebakKimia() {
35
+ return pickRandom(tebakKimiaDb);
36
+ }
37
+
38
+ function tebakKata() {
39
+ return pickRandom(tebakKataDb);
40
+ }
41
+
42
+ function tebakGambar() {
43
+ return pickRandom(tebakGambarDb);
44
+ }
45
+
46
+ function family100() {
47
+ return pickRandom(family100Db);
48
+ }
49
+
50
+ module.exports = {
51
+ asahOtak,
52
+ susunKata,
53
+ tebakBendera,
54
+ tebakKimia,
55
+ tebakKata,
56
+ tebakGambar,
57
+ family100
58
+ };