Compare commits

..

7 Commits

14 changed files with 265 additions and 156 deletions

View File

@@ -4,7 +4,4 @@ source 'https://rubygems.org'
gem 'discordrb', git: 'https://github.com/shardlab/discordrb.git', branch: 'main' gem 'discordrb', git: 'https://github.com/shardlab/discordrb.git', branch: 'main'
gem 'dotenv' gem 'dotenv'
gem 'pg' gem 'pg'
gem 'prawn' gem 'i18n'
gem 'prawn-table'
gem 'gruff'
gem 'rmagick'

View File

@@ -17,7 +17,7 @@ GEM
remote: https://rubygems.org/ remote: https://rubygems.org/
specs: specs:
base64 (0.3.0) base64 (0.3.0)
bigdecimal (3.3.1) concurrent-ruby (1.3.6)
domain_name (0.6.20240107) domain_name (0.6.20240107)
dotenv (3.2.0) dotenv (3.2.0)
event_emitter (0.2.6) event_emitter (0.2.6)
@@ -32,26 +32,20 @@ GEM
ffi (1.17.3-x86_64-darwin) ffi (1.17.3-x86_64-darwin)
ffi (1.17.3-x86_64-linux-gnu) ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl) ffi (1.17.3-x86_64-linux-musl)
gruff (0.29.0)
bigdecimal (>= 3.0)
histogram
rmagick (>= 5.5)
histogram (0.2.4.1)
http-accept (1.7.0) http-accept (1.7.0)
http-cookie (1.1.0) http-cookie (1.1.0)
domain_name (~> 0.5) domain_name (~> 0.5)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
logger (1.7.0) logger (1.7.0)
matrix (0.4.3)
mime-types (3.7.0) mime-types (3.7.0)
logger logger
mime-types-data (~> 3.2025, >= 3.2025.0507) mime-types-data (~> 3.2025, >= 3.2025.0507)
mime-types-data (3.2025.0924) mime-types-data (3.2025.0924)
mutex_m (0.3.0) mutex_m (0.3.0)
netrc (0.11.0) netrc (0.11.0)
observer (0.1.2)
opus-ruby (1.0.1) opus-ruby (1.0.1)
ffi ffi
pdf-core (0.10.0)
pg (1.6.3) pg (1.6.3)
pg (1.6.3-aarch64-linux) pg (1.6.3-aarch64-linux)
pg (1.6.3-aarch64-linux-musl) pg (1.6.3-aarch64-linux-musl)
@@ -59,23 +53,11 @@ GEM
pg (1.6.3-x86_64-darwin) pg (1.6.3-x86_64-darwin)
pg (1.6.3-x86_64-linux) pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl) pg (1.6.3-x86_64-linux-musl)
pkg-config (1.6.5)
prawn (2.5.0)
matrix (~> 0.4)
pdf-core (~> 0.10.0)
ttfunk (~> 1.8)
prawn-table (0.2.2)
prawn (>= 1.3.0, < 3.0.0)
rest-client (2.1.0) rest-client (2.1.0)
http-accept (>= 1.7.0, < 2.0) http-accept (>= 1.7.0, < 2.0)
http-cookie (>= 1.0.2, < 2.0) http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0) mime-types (>= 1.16, < 4.0)
netrc (~> 0.8) netrc (~> 0.8)
rmagick (6.1.5)
observer (~> 0.1)
pkg-config (~> 1.4)
ttfunk (1.8.0)
bigdecimal (~> 3.1)
websocket (1.2.11) websocket (1.2.11)
websocket-client-simple (0.9.0) websocket-client-simple (0.9.0)
base64 base64
@@ -99,15 +81,12 @@ PLATFORMS
DEPENDENCIES DEPENDENCIES
discordrb! discordrb!
dotenv dotenv
gruff i18n
pg pg
prawn
prawn-table
rmagick
CHECKSUMS CHECKSUMS
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
bigdecimal (3.3.1) sha256=eaa01e228be54c4f9f53bf3cc34fe3d5e845c31963e7fcc5bedb05a4e7d52218 concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
discordrb (3.7.2) discordrb (3.7.2)
discordrb-webhooks (3.7.2) discordrb-webhooks (3.7.2)
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933 domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
@@ -124,19 +103,15 @@ CHECKSUMS
ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5 ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56 ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
gruff (0.29.0) sha256=ab808cbf507abda7ffacd4ba5805a43c47ad0ec6aa2a7b125cf8a165110047a0
histogram (0.2.4.1) sha256=9a6e379172b88ea842ab71700a535dd037185a4e17abcce742c7444679ae2abc
http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126 http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19 http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203 logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56 mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
mime-types-data (3.2025.0924) sha256=f276bca15e59f35767cbcf2bc10e023e9200b30bd6a572c1daf7f4cc24994728 mime-types-data (3.2025.0924) sha256=f276bca15e59f35767cbcf2bc10e023e9200b30bd6a572c1daf7f4cc24994728
mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751 mutex_m (0.3.0) sha256=cfcb04ac16b69c4813777022fdceda24e9f798e48092a2b817eb4c0a782b0751
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
observer (0.1.2) sha256=d8a3107131ba661138d748e7be3dbafc0d82e732fffba9fccb3d7829880950ac
opus-ruby (1.0.1) sha256=8dc808d6773a488469374cccf252808b2ad7bc7e00229a319832f0e09012ce53 opus-ruby (1.0.1) sha256=8dc808d6773a488469374cccf252808b2ad7bc7e00229a319832f0e09012ce53
pdf-core (0.10.0) sha256=0a5d101e2063c01e3f941e1ee47cbb97f1adfc1395b58372f4f65f1300f3ce91
pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99 pg (1.6.3) sha256=1388d0563e13d2758c1089e35e973a3249e955c659592d10e5b77c468f628a99
pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea pg (1.6.3-aarch64-linux) sha256=0698ad563e02383c27510b76bf7d4cd2de19cd1d16a5013f375dd473e4be72ea
pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c pg (1.6.3-aarch64-linux-musl) sha256=06a75f4ea04b05140146f2a10550b8e0d9f006a79cdaf8b5b130cde40e3ecc2c
@@ -144,12 +119,7 @@ CHECKSUMS
pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6 pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6
pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d
pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009 pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009
pkg-config (1.6.5) sha256=33f9f81c5322983d22b439b8b672f27777b406fea23bfec74ff14bbeb42ec733
prawn (2.5.0) sha256=f4e20e3b4f30bf5b9ae37dad15eb421831594553aa930b2391b0fa0a99c43cb6
prawn-table (0.2.2) sha256=336d46e39e003f77bf973337a958af6a68300b941c85cb22288872dc2b36addb
rest-client (2.1.0) sha256=35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3 rest-client (2.1.0) sha256=35a6400bdb14fae28596618e312776c158f7ebbb0ccad752ff4fa142bf2747e3
rmagick (6.1.5) sha256=91c1734cc0effc3b1be18e6705ea0353d73cb492547002f542763fa08445acd1
ttfunk (1.8.0) sha256=a7cbc7e489cc46e979dde04d34b5b9e4f5c8f1ee5fc6b1a7be39b829919d20ca
websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737 websocket (1.2.11) sha256=b7e7a74e2410b5e85c25858b26b3322f29161e300935f70a0e0d3c35e0462737
websocket-client-simple (0.9.0) sha256=f9a37c5e4922b35a711e21e6d73ed1e25892efa47d183203ab2f5beb4e563109 websocket-client-simple (0.9.0) sha256=f9a37c5e4922b35a711e21e6d73ed1e25892efa47d183203ab2f5beb4e563109

View File

@@ -6,7 +6,7 @@ Frugality is a Discord Bot written in Ruby using `discordrb` and PostgreSQL. Its
1. Commands are separated into individual files in `src/commands`. This is the only place you'll need to add a new command. 1. Commands are separated into individual files in `src/commands`. This is the only place you'll need to add a new command.
2. Automatically loads and registers new command files on startup. 2. Automatically loads and registers new command files on startup.
3. Uses PostgreSQL to store data (User IDs, coins amount, reason, timestamps). 3. Uses PostgreSQL to store data (User IDs, coins amount, reason, timestamps).
4. Supports Discord's slash commands. 4. Supports Discord's slash commands, and embeds for financial reports.
## Prerequisites ## Prerequisites
Before running the bot, ensure you have the following installed on your system: Before running the bot, ensure you have the following installed on your system:
@@ -14,7 +14,6 @@ Before running the bot, ensure you have the following installed on your system:
* **Bundler** * **Bundler**
* **PostgreSQL** * **PostgreSQL**
* **Git** * **Git**
* **ImageMagick**
## Installation ## Installation
1. **Clone the repository:** 1. **Clone the repository:**
@@ -62,7 +61,7 @@ The bot requires a PostgreSQL database.
BOT_TOKEN=your_discord_bot_token_here BOT_TOKEN=your_discord_bot_token_here
TEST_SERVER_ID=your_discord_server_id TEST_SERVER_ID=your_discord_server_id
``` ```
*Note: Add a Server ID only if you're planning on updating the bot frequently, and want instant changes.* *Note: Add a Server ID only if you're planning on updating the bot frequently, and want instant changes to be seen. Otherwise, remove it from the .env file and every `server_id: ENV['TEST_SERVER_ID']` line from the `register_application_command` on every `src/commands/*.rb` file.*
## Usage ## Usage
To start the bot, you must use `bundle exec` to load the local dependencies: To start the bot, you must use `bundle exec` to load the local dependencies:

View File

@@ -15,10 +15,22 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
require 'discordrb' require 'discordrb'
require 'i18n'
require_relative 'database' require_relative 'database'
require_relative 'utils/locales_helper'
class FrugalityBot class FrugalityBot
def initialize def initialize
I18n.config.enforce_available_locales = false
locales_path = File.join(File.dirname(__dir__), 'locales')
I18n.load_path += Dir["#{locales_path}/*.yml"]
I18n.backend.load_translations
I18n.default_locale = :en
@bot = Discordrb::Bot.new( @bot = Discordrb::Bot.new(
token: ENV['BOT_TOKEN'], token: ENV['BOT_TOKEN'],
intents: [:servers, :server_messages] intents: [:servers, :server_messages]
@@ -27,7 +39,7 @@ class FrugalityBot
@db = Database.new @db = Database.new
load_commands load_commands
setup_events startup_bot
end end
def run def run
@@ -37,35 +49,22 @@ class FrugalityBot
private private
def load_commands def load_commands
# 1. We look for all .rb files in "src/commands/..." Dir["#{File.dirname(__FILE__)}/commands/**/*.rb"].each do |file|
comm_files = Dir[File.join(__dir__, 'commands', '*.rb')] require file
comm_files.each do |file|
require file # We import the file
# We convert filename to module name
# This mean that 'echo.rb' turns into 'Echo'
# 'server_info' would turn into 'ServerInfo'
filename = File.basename(file, '.rb')
module_name = filename.split('_').map(&:capitalize).join
begin
# We find the module inside 'Commands' namespace
comm_module = Commands.const_get(module_name)
# Register the command
comm_module.register(@bot, @db)
puts "Loaded command: #{module_name}"
rescue NameError => e
puts "Could not load #{filename}: Module 'Commands::#{module_name}' was not found."
rescue StandardError => e
puts "Error loading: #{filename}: #{e.message}"
end
end
puts "Commands loaded."
end end
def setup_events Commands.constants.each do |const|
cmd = Commands.const_get(const)
if cmd.is_a?(Module) && cmd.respond_to?(:register)
cmd.register(@bot, @db)
puts "Loaded command: #{const}"
sleep(1.5)
end
end
end
def startup_bot
@bot.ready do @bot.ready do
puts "#{@bot.profile.username} is online" puts "#{@bot.profile.username} is online"
@bot.update_status("online", "Checking the economy...", nil, 0, false, 0) @bot.update_status("online", "Checking the economy...", nil, 0, false, 0)

View File

@@ -14,24 +14,39 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
require 'i18n'
module Commands module Commands
module Add module Add
extend self extend self
def register(bot, db) def register(bot, db)
bot.register_application_command(:add, 'Adds money to the wallet.', server_id: ENV['TEST_SERVER_ID']) do |cmd| command_key = :add
cmd.integer('amount', 'The amount you want to add.', required: true) arg_amount_key = :amount
cmd.string('reason', "Reason you're adding money to the wallet. Leave empty for default.", required: false) arg_reason_key = :reason
cmd_desc = I18n.t('commands.add.description', locale: :en)
amount_desc = I18n.t('commands.add.args.amount_desc', locale: :en)
reason_desc = I18n.t('commands.add.args.reason_desc', locale: :en)
bot.register_application_command(command_key, cmd_desc, server_id: ENV['TEST_SERVER_ID']) do |cmd|
cmd.integer(arg_amount_key, amount_desc, required: true)
cmd.string(arg_reason_key, reason_desc, required: false)
end end
bot.application_command(:add) do |event| bot.application_command(command_key) do |event|
db_lang = db.get_language(event.user.id)
discord_lang = event.locale.to_s[0..1]
I18n.locale = db_lang || (I18n.available_locales.include?(discord_lang.to_sym) ? discord_lang : :en)
user_id = event.user.id user_id = event.user.id
amount = event.options['amount'] amount = event.options['amount']
reason = event.options['reason'] ||= 'transaction' reason = event.options['reason'] || 'income'
db.update_balance(user_id, amount, reason) db.update_balance(user_id, amount, reason)
event.respond(content: "Added: #{amount} to the wallet.\nReason: #{reason}") msg = I18n.t('responses.add.success', amount: amount, reason: reason, default: "Added #{amount} (Reason: #{reason})")
event.respond(content: msg)
end end
end end
end end

View File

@@ -15,13 +15,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
module Commands module Commands
module Currency module Balance
extend self extend self
def register(bot, db) def register(bot, db)
bot.register_application_command(:currency, 'Get your currency', server_id: ENV['TEST_SERVER_ID']) bot.register_application_command(:balance, 'Get your currency', server_id: ENV['TEST_SERVER_ID'])
bot.application_command(:currency) do |event| bot.application_command(:balance) do |event|
# 1. Get the User ID from the event # 1. Get the User ID from the event
user_id = event.user.id user_id = event.user.id

View File

@@ -14,9 +14,9 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>. # along with this program. If not, see <https://www.gnu.org/licenses/>.
require 'prawn' require 'net/http'
require 'prawn/table' require 'uri'
require 'gruff' require 'json'
module Commands module Commands
module Statement module Statement
@@ -36,70 +36,52 @@ module Commands
user_id = event.user.id user_id = event.user.id
period = event.options['period'] period = event.options['period']
# For income we fetch the data as stats[:income]
# For expenses we fetch the data as stats[:expenses]
# For net income we fetch the data as stats[:net]
stats = db.fetch_report(user_id, period) stats = db.fetch_report(user_id, period)
# We generate a Graph chart_config = {
g = Gruff::Pie.new type: 'doughnut',
g.title = "Report" data: {
g.theme = Gruff::Themes::PASTEL labels: ['Income', 'Expenses'],
datasets: [{
# We add data data: [stats[:income], stats[:expenses].abs],
g.data('Income', stats[:income]) backgroundColor: ['#9AF5AA', '#F55D5D'],
g.data('Expenses', stats[:expenses].abs) borderWidth: 1
}]
# We save the graph temporarily },
chart_path = "/tmp/chart_#{user_id}.png" options: {
g.write(chart_path) plugins: {
doughnutlabel: {
# We generate a PDF labels: [
pdf_path = "/tmp/statement_#{user_id}.pdf" { text: stats[:net].to_s, font: { size: 20}},
{ text: 'Net', font: { size: 10 }}
Prawn::Document.generate(pdf_path) do |pdf|
pdf.font "Helvetica"
# This should be the Header
pdf.text "#{event.user.name}'s Financial Statement", size: 24, style: :bold, align: :center
pdf.move_down 10
pdf.text "Period: #{period}", align: :center
pdf.move_down 20
# Summary table goes here
data = [
["Category", "$$$"],
["Total Income", "#{stats[:income]} coins"],
["Total Expenses", "#{stats[:expenses].abs} coins"],
["Net Change", "#{stats[:net]} coins"]
] ]
}
}
}
}
pdf.table(data, position: :center) do encoded_config = URI.encode_www_form_component(chart_config.to_json)
row(0).font_style = :bold chart_url = "https://quickchart.io/chart?c=#{encoded_config}&w=500&h=300&bkg=rgb(255,255,255)"
row(0).background_color = "CCCCCC"
self.header = true
self.width = 400
end
pdf.move_down 30 color_generated = ("0x" + Random.bytes(3).unpack1('H*')).to_i(16)
pdf.text "Visual Breakdown:", style: :bold embed_generated = {
pdf.move_down 10 title: "Financial Statement",
# We embed the image chart description: "Report generated for **#{event.user.name}** (#{period})",
pdf.image chart_path, width: 400, position: :center color: color_generated,
fields: [
{ name: "Income", value: "#{stats[:income]} coins", inline: true },
{ name: "Expenses", value: "#{stats[:expenses].abs} coins", inline: true },
{ name: "Net Change", value: "#{stats[:net]} coins", inline: false }
],
image: { url: chart_url },
footer: { text: "By FrugalityBot with QuickChart." }
}
# Footer event.edit_response(embeds: [embed_generated])
pdf.move_down 10
pdf.text "Generated by Frugality using Gruff and Prawn", size: 10, align: :center, color: "888888"
end
event.edit_response(content: "Report generated. Sending...")
# Send the report to the user
File.open(pdf_path, 'r') do |f|
event.channel.send_file(f, caption: "Here's your report.")
end
# Cleanup
File.delete(chart_path) if File.exist?(chart_path)
File.delete(pdf_path) if File.exist?(pdf_path)
end end
end end
end end

View File

@@ -0,0 +1,45 @@
# FrugalityBot
# Copyright (C) 2026 Eri (csxkdv/nxkdv) nxkdv@thenight.club
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
require 'securerandom'
module Commands
module Embed
extend self
def register(bot, _db)
bot.register_application_command(:embed, 'This is an embed testing', server_id: ENV['TEST_SERVER_ID']) do |cmd|
end
bot.application_command(:embed) do |event|
color_generated = ("0x" + Random.bytes(3).unpack1('H*')).to_i(16)
embed_generated = {
title: "Title testing.",
description: "Description testing.",
color: color_generated,
fields: [
{ name: "Field 1", value: "Value for field 1", inline: true },
{ name: "Field 2", value: "Value for field 2", inline: false }
],
image: { url: "https://i.kym-cdn.com/photos/images/original/002/349/700/e38.jpg" },
footer: { text: "Footer testing." }
}
event.respond(embeds: [embed_generated])
end
end
end
end

View File

@@ -50,19 +50,23 @@ class Database
@conn.exec(sql_wallet) @conn.exec(sql_wallet)
@conn.exec(sql_ledger) @conn.exec(sql_ledger)
@conn.exec(sql_index) @conn.exec(sql_index)
begin
@conn.exec("ALTER TABLE wallets ADD COLUMN IF NOT EXISTS locale VARCHAR(5) DEFAULT 'en'")
rescue PG::Error => e
puts "Migration note: #{e.message}"
end
puts "Database tables have been initialized." puts "Database tables have been initialized."
end end
# We pass the user_id
def get_currency(user_id) def get_currency(user_id)
# 1. Run the query using parameters ($1) to prevent SQL injection # Run the query using parameters ($1) to prevent SQL injection
result = @conn.exec_params("SELECT amount FROM wallets WHERE user_id = $1", [user_id]) result = @conn.exec_params("SELECT amount FROM wallets WHERE user_id = $1", [user_id])
# 2. Check if the user exists
if result.num_tuples.zero? if result.num_tuples.zero?
return 0 # User has no money/row yet return 0 # User has no money/row yet
else else
# 3. Return the value (don't print it)
return result[0]['amount'].to_i return result[0]['amount'].to_i
end end
end end
@@ -106,4 +110,23 @@ class Database
net: row['net_change'].to_i net: row['net_change'].to_i
} }
end end
def get_language(user_id)
result = @conn.exec("SELECT locale FROM wallets WHERE user_id = $1", [user_id])
return nil if result.num_tuples.zero?
return result[0]['locale']
end
def set_language(user_id, locale)
sql = <<~SQL
INSERT INTO wallets(user_id, amount, locale)
VALUES ($1, 0, $2)
ON CONFLICT (user_id)
DO UPDATE SET locale = $2
SQL
@conn.exec(sql, [user_id, locale])
end
end end

19
src/locales/en.yml Normal file
View File

@@ -0,0 +1,19 @@
en:
commands:
add:
name: "add"
description: "Add money to the wallet."
args:
amount: "amount"
amount_desc: "How much money you're adding to the wallet."
reason: "reason"
reason_desc: "Reason you're adding money. Leave blank for deafult."
balance:
name: "balance"
description: "Check your wallet."
responses:
add:
success: "Added **%{amount}** coins to the wallet. Reason: **%{reason}**"
balance:
view: "You have **%{balance}** coins."

19
src/locales/es.yml Normal file
View File

@@ -0,0 +1,19 @@
es:
commands:
add:
name: "añadir"
description: "Añade monedas a la billetera."
args:
amount: "cantidad"
amount_desc: "La cantidad de monedas que añades."
reason: "motivo"
reason_desc: "El motivo por el cual añades monedas. Déjalo vacío para default."
balance:
name: "saldo"
description: "Revisa tu billetera."
responses:
add:
success: "Se añadieron **%{amount}** monedas a la billetera. Motivo: **%{reason}**"
balance:
view: "Tienes **%{balance}** monedas."

View File

@@ -0,0 +1,41 @@
# FrugalityBot
# Copyright (C) 2026 Eri (csxkdv/nxkdv) nxkdv@thenight.club
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
require 'i18n'
module LocalesHelper
def self.generate(key_path)
locales_map = {}
discord_map = {
:en => 'en-US',
:es => 'es-ES'
}
I18n.available_locales.each do |lang|
next if lang == :en
text = I18n.t(key_path, locale: lang, default: nil)
if text && text != 'nil'
discord_code = discord_map[lang] || lang.to_s
locales_map[discord_code] = text
end
end
return locales_map
end
end