feat: micro-transactions

This commit is contained in:
Milo
2026-02-06 17:47:18 +01:00
parent 1371200041
commit c4c8eaf5d6
6 changed files with 439 additions and 88 deletions

215
package-lock.json generated
View File

@@ -21,6 +21,7 @@
"openai": "^4.104.0", "openai": "^4.104.0",
"pokersolver": "^2.1.4", "pokersolver": "^2.1.4",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"stripe": "^20.3.0",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
@@ -36,15 +37,15 @@
} }
}, },
"node_modules/@discordjs/builders": { "node_modules/@discordjs/builders": {
"version": "1.11.3", "version": "1.13.1",
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.3.tgz", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.13.1.tgz",
"integrity": "sha512-p3kf5eV49CJiRTfhtutUCeivSyQ/l2JlKodW1ZquRwwvlOWmG9+6jFShX6x8rUiYhnP6wKI96rgN/SXMy5e5aw==", "integrity": "sha512-cOU0UDHc3lp/5nKByDxkmRiNZBpdp0kx55aarbiAfakfKJHlxv/yFW1zmIqCAmwH5CRlrH9iMFKJMpvW4DPB+w==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/formatters": "^0.6.1", "@discordjs/formatters": "^0.6.2",
"@discordjs/util": "^1.1.1", "@discordjs/util": "^1.2.0",
"@sapphire/shapeshift": "^4.0.0", "@sapphire/shapeshift": "^4.0.0",
"discord-api-types": "^0.38.16", "discord-api-types": "^0.38.33",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
"ts-mixer": "^6.0.4", "ts-mixer": "^6.0.4",
"tslib": "^2.6.3" "tslib": "^2.6.3"
@@ -66,12 +67,12 @@
} }
}, },
"node_modules/@discordjs/formatters": { "node_modules/@discordjs/formatters": {
"version": "0.6.1", "version": "0.6.2",
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
"integrity": "sha512-5cnX+tASiPCqCWtFcFslxBVUaCetB0thvM/JyavhbXInP1HJIEU+Qv/zMrnuwSsX3yWH2lVXNJZeDK3EiP4HHg==", "integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"discord-api-types": "^0.38.1" "discord-api-types": "^0.38.33"
}, },
"engines": { "engines": {
"node": ">=16.11.0" "node": ">=16.11.0"
@@ -116,10 +117,13 @@
} }
}, },
"node_modules/@discordjs/util": { "node_modules/@discordjs/util": {
"version": "1.1.1", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
"integrity": "sha512-eddz6UnOBEB1oITPinyrB2Pttej49M9FZQY8NxgEvc3tq6ZICZ19m70RsmzRdDHk80O9NoYN/25AqJl8vPVf/g==", "integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": {
"discord-api-types": "^0.38.33"
},
"engines": { "engines": {
"node": ">=18" "node": ">=18"
}, },
@@ -865,29 +869,58 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "1.20.3", "version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"content-type": "~1.0.5", "content-type": "~1.0.5",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"destroy": "1.2.0", "destroy": "~1.2.0",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.4.24", "iconv-lite": "~0.4.24",
"on-finished": "2.4.1", "on-finished": "~2.4.1",
"qs": "6.13.0", "qs": "~6.14.0",
"raw-body": "2.5.2", "raw-body": "~2.5.3",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8", "node": ">= 0.8",
"npm": "1.2.8000 || >= 1.4.16" "npm": "1.2.8000 || >= 1.4.16"
} }
}, },
"node_modules/body-parser/node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/body-parser/node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/brace-expansion": { "node_modules/brace-expansion": {
"version": "1.1.12", "version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -1248,9 +1281,9 @@
} }
}, },
"node_modules/discord-api-types": { "node_modules/discord-api-types": {
"version": "0.38.23", "version": "0.38.38",
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.23.tgz", "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.38.tgz",
"integrity": "sha512-C8VjK0yxBUq1dakxGpUXQm4VSC7R+aaD2SIr3paj2a0bP/LRok1AqHiezp30GruK6Ba9FtQAKqYUMJPzsqv7IQ==", "integrity": "sha512-7qcM5IeZrfb+LXW07HvoI5L+j4PQeMZXEkSm1htHAHh4Y9JSMXBWjy/r7zmUCOj4F7zNjMcm7IMWr131MT2h0Q==",
"license": "MIT", "license": "MIT",
"workspaces": [ "workspaces": [
"scripts/actions/documentation" "scripts/actions/documentation"
@@ -1266,19 +1299,19 @@
} }
}, },
"node_modules/discord.js": { "node_modules/discord.js": {
"version": "14.22.1", "version": "14.25.1",
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.22.1.tgz", "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.25.1.tgz",
"integrity": "sha512-3k+Kisd/v570Jr68A1kNs7qVhNehDwDJAPe4DZ2Syt+/zobf9zEcuYFvsfIaAOgCa0BiHMfOOKQY4eYINl0z7w==", "integrity": "sha512-2l0gsPOLPs5t6GFZfQZKnL1OJNYFcuC/ETWsW4VtKVD/tg4ICa9x+jb9bkPffkMdRpRpuUaO/fKkHCBeiCKh8g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@discordjs/builders": "^1.11.2", "@discordjs/builders": "^1.13.0",
"@discordjs/collection": "1.5.3", "@discordjs/collection": "1.5.3",
"@discordjs/formatters": "^0.6.1", "@discordjs/formatters": "^0.6.2",
"@discordjs/rest": "^2.6.0", "@discordjs/rest": "^2.6.0",
"@discordjs/util": "^1.1.1", "@discordjs/util": "^1.2.0",
"@discordjs/ws": "^1.2.3", "@discordjs/ws": "^1.2.3",
"@sapphire/snowflake": "3.5.3", "@sapphire/snowflake": "3.5.3",
"discord-api-types": "^0.38.16", "discord-api-types": "^0.38.33",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"lodash.snakecase": "4.1.1", "lodash.snakecase": "4.1.1",
"magic-bytes.js": "^1.10.0", "magic-bytes.js": "^1.10.0",
@@ -1729,39 +1762,39 @@
} }
}, },
"node_modules/express": { "node_modules/express": {
"version": "4.21.2", "version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"accepts": "~1.3.8", "accepts": "~1.3.8",
"array-flatten": "1.1.1", "array-flatten": "1.1.1",
"body-parser": "1.20.3", "body-parser": "~1.20.3",
"content-disposition": "0.5.4", "content-disposition": "~0.5.4",
"content-type": "~1.0.4", "content-type": "~1.0.4",
"cookie": "0.7.1", "cookie": "~0.7.1",
"cookie-signature": "1.0.6", "cookie-signature": "~1.0.6",
"debug": "2.6.9", "debug": "2.6.9",
"depd": "2.0.0", "depd": "2.0.0",
"encodeurl": "~2.0.0", "encodeurl": "~2.0.0",
"escape-html": "~1.0.3", "escape-html": "~1.0.3",
"etag": "~1.8.1", "etag": "~1.8.1",
"finalhandler": "1.3.1", "finalhandler": "~1.3.1",
"fresh": "0.5.2", "fresh": "~0.5.2",
"http-errors": "2.0.0", "http-errors": "~2.0.0",
"merge-descriptors": "1.0.3", "merge-descriptors": "1.0.3",
"methods": "~1.1.2", "methods": "~1.1.2",
"on-finished": "2.4.1", "on-finished": "~2.4.1",
"parseurl": "~1.3.3", "parseurl": "~1.3.3",
"path-to-regexp": "0.1.12", "path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7", "proxy-addr": "~2.0.7",
"qs": "6.13.0", "qs": "~6.14.0",
"range-parser": "~1.2.1", "range-parser": "~1.2.1",
"safe-buffer": "5.2.1", "safe-buffer": "5.2.1",
"send": "0.19.0", "send": "~0.19.0",
"serve-static": "1.16.2", "serve-static": "~1.16.2",
"setprototypeof": "1.2.0", "setprototypeof": "1.2.0",
"statuses": "2.0.1", "statuses": "~2.0.1",
"type-is": "~1.6.18", "type-is": "~1.6.18",
"utils-merge": "1.0.1", "utils-merge": "1.0.1",
"vary": "~1.1.2" "vary": "~1.1.2"
@@ -2601,12 +2634,12 @@
} }
}, },
"node_modules/jws": { "node_modules/jws": {
"version": "4.0.0", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"jwa": "^2.0.0", "jwa": "^2.0.1",
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
@@ -2651,9 +2684,9 @@
} }
}, },
"node_modules/lodash": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.23",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.merge": { "node_modules/lodash.merge": {
@@ -3290,12 +3323,12 @@
} }
}, },
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.14.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"side-channel": "^1.0.6" "side-channel": "^1.1.0"
}, },
"engines": { "engines": {
"node": ">=0.6" "node": ">=0.6"
@@ -3314,20 +3347,49 @@
} }
}, },
"node_modules/raw-body": { "node_modules/raw-body": {
"version": "2.5.2", "version": "2.5.3",
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "3.1.2", "bytes": "~3.1.2",
"http-errors": "2.0.0", "http-errors": "~2.0.1",
"iconv-lite": "0.4.24", "iconv-lite": "~0.4.24",
"unpipe": "1.0.0" "unpipe": "~1.0.0"
}, },
"engines": { "engines": {
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/raw-body/node_modules/http-errors": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
"license": "MIT",
"dependencies": {
"depd": "~2.0.0",
"inherits": "~2.0.4",
"setprototypeof": "~1.2.0",
"statuses": "~2.0.2",
"toidentifier": "~1.0.1"
},
"engines": {
"node": ">= 0.8"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/raw-body/node_modules/statuses": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"node_modules/rc": { "node_modules/rc": {
"version": "1.2.8", "version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
@@ -3910,6 +3972,23 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/stripe": {
"version": "20.3.0",
"resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.0.tgz",
"integrity": "sha512-DYzcmV1MfYhycr1GwjCjeQVYk9Gu8dpxyTlu7qeDCsuguug7oUTxPsUQuZeSf/OPzK7pofqobvOKVqAwlpgf/Q==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"peerDependencies": {
"@types/node": ">=16"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
}
}
},
"node_modules/supports-color": { "node_modules/supports-color": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",

View File

@@ -28,6 +28,7 @@
"openai": "^4.104.0", "openai": "^4.104.0",
"pokersolver": "^2.1.4", "pokersolver": "^2.1.4",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"stripe": "^20.3.0",
"unique-names-generator": "^4.7.1", "unique-names-generator": "^4.7.1",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },

View File

@@ -1,4 +1,5 @@
import { sleep } from "openai/core"; import { sleep } from "openai/core";
import { AttachmentBuilder } from "discord.js";
import { import {
buildAiMessages, buildAiMessages,
buildParticipantsMap, buildParticipantsMap,
@@ -52,10 +53,10 @@ export async function handleMessageCreate(message, client, io) {
// --- Main Guild Features (Points & Slowmode) --- // --- Main Guild Features (Points & Slowmode) ---
if (message.guildId === process.env.GUILD_ID) { if (message.guildId === process.env.GUILD_ID) {
// Award points for activity // Award points for activity
const pointsAwarded = channelPointsHandler(message); // const pointsAwarded = channelPointsHandler(message);
if (pointsAwarded) { // if (pointsAwarded) {
io.emit("data-updated", { table: "users", action: "update" }); // io.emit("data-updated", { table: "users", action: "update" });
} // }
// Enforce active slowmodes // Enforce active slowmodes
const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes); const wasSlowmoded = await slowmodesHandler(message, activeSlowmodes);
@@ -245,7 +246,10 @@ async function handleAdminCommands(message) {
try { try {
const stmt = flopoDB.prepare(sqlCommand); const stmt = flopoDB.prepare(sqlCommand);
const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") ? stmt.all() : stmt.run(); const result = sqlCommand.trim().toUpperCase().startsWith("SELECT") ? stmt.all() : stmt.run();
message.reply("```json\n" + JSON.stringify(result, null, 2).substring(0, 1900) + "\n```"); const jsonString = JSON.stringify(result, null, 2);
const buffer = Buffer.from(jsonString, "utf-8");
const attachment = new AttachmentBuilder(buffer, { name: "sql-result.json" });
message.reply({ content: "SQL query executed successfully:", files: [attachment] });
} catch (e) { } catch (e) {
message.reply(`SQL Error: ${e.message}`); message.reply(`SQL Error: ${e.message}`);
} }

View File

@@ -269,6 +269,49 @@ flopoDB.exec(`
score score
INTEGER INTEGER
); );
CREATE TABLE IF NOT EXISTS transactions
(
id
TEXT
PRIMARY
KEY,
session_id
TEXT
UNIQUE
NOT
NULL,
user_id
TEXT
REFERENCES
users
NOT
NULL,
coins_amount
INTEGER
NOT
NULL,
amount_cents
INTEGER
NOT
NULL,
currency
TEXT
DEFAULT
'eur',
customer_email
TEXT,
customer_name
TEXT,
payment_status
TEXT
NOT
NULL,
created_at
DATETIME
DEFAULT
CURRENT_TIMESTAMP
);
`); `);
/* ----------------------------------------------------- /* -----------------------------------------------------
@@ -566,6 +609,52 @@ export const stmtSOTDStats = flopoDB.prepare(`
`); `);
stmtSOTDStats.run(); stmtSOTDStats.run();
export const stmtTransactions = flopoDB.prepare(`
CREATE TABLE IF NOT EXISTS transactions
(
id
TEXT
PRIMARY
KEY,
session_id
TEXT
UNIQUE
NOT
NULL,
user_id
TEXT
REFERENCES
users
NOT
NULL,
coins_amount
INTEGER
NOT
NULL,
amount_cents
INTEGER
NOT
NULL,
currency
TEXT
DEFAULT
'eur',
customer_email
TEXT,
customer_name
TEXT,
payment_status
TEXT
NOT
NULL,
created_at
DATETIME
DEFAULT
CURRENT_TIMESTAMP
)
`);
stmtTransactions.run();
/* ------------------------- /* -------------------------
USER statements USER statements
----------------------------*/ ----------------------------*/
@@ -861,3 +950,23 @@ export async function pruneOldLogs() {
transaction(); transaction();
} }
/* -------------------------
TRANSACTION statements
----------------------------*/
export const insertTransaction = flopoDB.prepare(
`INSERT INTO transactions (id, session_id, user_id, coins_amount, amount_cents, currency, customer_email, customer_name, payment_status)
VALUES (@id, @session_id, @user_id, @coins_amount, @amount_cents, @currency, @customer_email, @customer_name, @payment_status)`,
);
export const getTransactionBySessionId = flopoDB.prepare(
`SELECT * FROM transactions WHERE session_id = ?`,
);
export const getAllTransactions = flopoDB.prepare(
`SELECT * FROM transactions ORDER BY created_at DESC`,
);
export const getUserTransactions = flopoDB.prepare(
`SELECT * FROM transactions WHERE user_id = ? ORDER BY created_at DESC`,
);

View File

@@ -37,6 +37,9 @@ app.post("/interactions", verifyKeyMiddleware(process.env.PUBLIC_KEY), async (re
await handleInteraction(req, res, client); await handleInteraction(req, res, client);
}); });
// Stripe webhook endpoint needs raw body for signature verification
app.use("/api/buy-coins", express.raw({ type: "application/json" }));
// JSON Body Parser Middleware // JSON Body Parser Middleware
app.use(express.json()); app.use(express.json());

View File

@@ -1,5 +1,6 @@
import express from "express"; import express from "express";
import { sleep } from "openai/core"; import { sleep } from "openai/core";
import Stripe from "stripe";
// --- Database Imports --- // --- Database Imports ---
import { import {
@@ -21,6 +22,10 @@ import {
queryDailyReward, queryDailyReward,
updateSkin, updateSkin,
updateUserCoins, updateUserCoins,
insertTransaction,
getTransactionBySessionId,
getAllTransactions,
getUserTransactions,
} from "../../database/index.js"; } from "../../database/index.js";
// --- Game State Imports --- // --- Game State Imports ---
@@ -1277,24 +1282,174 @@ export function apiRoutes(client, io) {
} }
}); });
// --- Admin Routes --- // Fixed coin offers - server-side source of truth
const COIN_OFFERS = [
{ id: "offer_5000", coins: 5000, amount_cents: 99, label: "5 000 FlopoCoins" },
{ id: "offer_20000", coins: 20000, amount_cents: 299, label: "20 000 FlopoCoins" },
{ id: "offer_40000", coins: 40000, amount_cents: 499, label: "40 000 FlopoCoins" },
{ id: "offer_100000", coins: 100000, amount_cents: 999, label: "100 000 FlopoCoins" },
];
router.post("/buy-coins", (req, res) => { router.get("/coin-offers", (req, res) => {
const { commandUserId, coins } = req.body; res.json({ offers: COIN_OFFERS });
const user = getUser.get(commandUserId); });
if (!user) return res.status(404).json({ error: "User not found" });
const newCoins = user.coins + coins; router.post("/create-checkout-session", async (req, res) => {
updateUserCoins.run({ id: commandUserId, coins: newCoins }); const { userId, offerId } = req.body;
insertLog.run({
id: `${commandUserId}-buycoins-${Date.now()}`,
user_id: commandUserId,
action: "BUY_COINS_ADMIN",
coins_amount: coins,
user_new_amount: newCoins,
});
res.status(200).json({ message: `Added ${coins} coins.` }); if (!userId || !offerId) {
return res.status(400).json({ error: "Missing required fields: userId, offerId" });
}
const offer = COIN_OFFERS.find((o) => o.id === offerId);
if (!offer) {
return res.status(400).json({ error: "Invalid offer" });
}
const user = getUser.get(userId);
if (!user) {
return res.status(404).json({ error: "User not found" });
}
try {
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
const FLAPI_URL = process.env.DEV_SITE === "true" ? process.env.FLAPI_URL_DEV : process.env.FLAPI_URL;
const session = await stripe.checkout.sessions.create({
payment_method_types: ['card'],
line_items: [
{
price_data: {
currency: 'eur',
product_data: {
name: offer.label,
description: `Achat de ${offer.label} pour FlopoBot`,
},
unit_amount: offer.amount_cents,
},
quantity: 1,
},
],
mode: 'payment',
success_url: `${FLAPI_URL}/payment-success?session_id={CHECKOUT_SESSION_ID}`,
cancel_url: `${FLAPI_URL}/dashboard`,
metadata: {
userId: userId,
coins: offer.coins.toString(),
},
});
console.log(`[CHECKOUT] New session for user ${userId}: ${session.id}, offer: ${offer.id} (${offer.coins} coins for ${offer.amount_cents} cents)`);
res.json({ sessionId: session.id });
} catch (error) {
console.error("Error creating checkout session:", error);
res.status(500).json({ error: "Failed to create checkout session" });
}
});
router.post("/buy-coins", async (req, res) => {
const sig = req.headers['stripe-signature'];
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET;
if (!endpointSecret) {
console.error("STRIPE_WEBHOOK_SECRET not configured");
return res.status(500).json({ error: "Webhook not configured" });
}
let event;
try {
// Verify webhook signature - requires raw body
// Note: You need to configure Express to preserve raw body for this route
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
} catch (err) {
console.error(`Webhook signature verification failed: ${err.message}`);
return res.status(400).json({ error: `Webhook Error: ${err.message}` });
}
// Handle the event
if (event.type === 'checkout.session.completed') {
const session = event.data.object;
// Extract metadata from the checkout session
const commandUserId = session.metadata?.userId;
const expectedCoins = parseInt(session.metadata?.coins);
const amountPaid = session.amount_total; // in cents
const currency = session.currency;
const customerEmail = session.customer_details?.email;
const customerName = session.customer_details?.name;
// Validate metadata exists
if (!commandUserId || !expectedCoins) {
console.error("Missing userId or coins in session metadata");
return res.status(400).json({ error: "Invalid session metadata" });
}
// Verify payment was successful
if (session.payment_status !== 'paid') {
console.error(`Payment not completed for session ${session.id}`);
return res.status(400).json({ error: "Payment not completed" });
}
// Check for duplicate processing (idempotency)
const existingTransaction = getTransactionBySessionId.get(session.id);
if (existingTransaction) {
console.log(`Payment already processed: ${session.id}`);
return res.status(200).json({ message: "Already processed" });
}
// Get user
const user = getUser.get(commandUserId);
if (!user) {
console.error(`User not found: ${commandUserId}`);
return res.status(404).json({ error: "User not found" });
}
// Update coins
const newCoins = user.coins + expectedCoins;
updateUserCoins.run({ id: commandUserId, coins: newCoins });
// Insert transaction record
const transactionId = `${commandUserId}-transaction-${Date.now()}`;
insertTransaction.run({
id: transactionId,
session_id: session.id,
user_id: commandUserId,
coins_amount: expectedCoins,
amount_cents: amountPaid,
currency: currency,
customer_email: customerEmail,
customer_name: customerName,
payment_status: session.payment_status,
});
// Insert log entry
insertLog.run({
id: `${commandUserId}-buycoins-${Date.now()}`,
user_id: commandUserId,
action: "BUY_COINS",
target_user_id: null,
coins_amount: expectedCoins,
user_new_amount: newCoins,
});
console.log(`Payment processed: ${commandUserId} purchased ${expectedCoins} coins for ${amountPaid/100} ${currency}`);
// Notify user via Discord if possible
try {
const discordUser = await client.users.fetch(commandUserId);
await discordUser.send(`✅ Votre achat de ${expectedCoins} FlopoCoins a été confirmé ! Merci pour votre soutien !`);
} catch (e) {
console.log(`Could not DM user ${commandUserId}:`, e.message);
}
return res.status(200).json({ message: `Added ${expectedCoins} coins.` });
}
// Return 200 for unhandled event types (Stripe requires this)
res.status(200).json({ received: true });
}); });
return router; return router;