Compare commits

..

5 Commits

14 changed files with 179 additions and 54 deletions

View File

@@ -3,4 +3,5 @@ source 'https://rubygems.org'
gem 'discordrb', git: 'https://github.com/shardlab/discordrb.git', branch: 'main'
gem 'dotenv'
gem 'pg'
gem 'pg'
gem 'i18n'

View File

@@ -17,6 +17,7 @@ GEM
remote: https://rubygems.org/
specs:
base64 (0.3.0)
concurrent-ruby (1.3.6)
domain_name (0.6.20240107)
dotenv (3.2.0)
event_emitter (0.2.6)
@@ -34,6 +35,8 @@ GEM
http-accept (1.7.0)
http-cookie (1.1.0)
domain_name (~> 0.5)
i18n (1.14.8)
concurrent-ruby (~> 1.0)
logger (1.7.0)
mime-types (3.7.0)
logger
@@ -78,10 +81,12 @@ PLATFORMS
DEPENDENCIES
discordrb!
dotenv
i18n
pg
CHECKSUMS
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
discordrb (3.7.2)
discordrb-webhooks (3.7.2)
domain_name (0.6.20240107) sha256=5f693b2215708476517479bf2b3802e49068ad82167bcd2286f899536a17d933
@@ -100,6 +105,7 @@ CHECKSUMS
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
http-accept (1.7.0) sha256=c626860682bfbb3b46462f8c39cd470fd7b0584f61b3cc9df5b2e9eb9972a126
http-cookie (1.1.0) sha256=38a5e60d1527eebc396831b8c4b9455440509881219273a6c99943d29eadbb19
i18n (1.14.8) sha256=285778639134865c5e0f6269e0b818256017e8cde89993fdfcbfb64d088824a5
logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
mime-types (3.7.0) sha256=dcebf61c246f08e15a4de34e386ebe8233791e868564a470c3fe77c00eed5e56
mime-types-data (3.2025.0924) sha256=f276bca15e59f35767cbcf2bc10e023e9200b30bd6a572c1daf7f4cc24994728

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.
2. Automatically loads and registers new command files on startup.
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
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**
* **PostgreSQL**
* **Git**
* **ImageMagick**
## Installation
1. **Clone the repository:**
@@ -62,7 +61,7 @@ The bot requires a PostgreSQL database.
BOT_TOKEN=your_discord_bot_token_here
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
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/>.
require 'discordrb'
require 'i18n'
require_relative 'database'
require_relative 'utils/locales_helper'
class FrugalityBot
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(
token: ENV['BOT_TOKEN'],
intents: [:servers, :server_messages]
@@ -27,7 +39,7 @@ class FrugalityBot
@db = Database.new
load_commands
setup_events
startup_bot
end
def run
@@ -37,34 +49,22 @@ class FrugalityBot
private
def load_commands
# 1. We look for all .rb files in "src/commands/..."
comm_files = Dir[File.join(__dir__, 'commands', '*.rb')]
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
comm_module = Commands.const_get(module_name)
comm_module.register(@bot, @db)
puts "Loaded command: #{module_name}"
sleep(1.5)
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
Dir["#{File.dirname(__FILE__)}/commands/**/*.rb"].each do |file|
require file
end
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
puts "Commands loaded."
end
def setup_events
def startup_bot
@bot.ready do
puts "#{@bot.profile.username} is online"
@bot.update_status("online", "Checking the economy...", nil, 0, false, 0)

View File

@@ -14,25 +14,40 @@
# 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 Commands
module Add
extend self
module Add
extend self
def register(bot, db)
command_key = :add
arg_amount_key = :amount
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
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
amount = event.options['amount']
reason = event.options['reason'] || 'income'
def register(bot, db)
bot.register_application_command(:add, 'Adds money to the wallet.', server_id: ENV['TEST_SERVER_ID']) do |cmd|
cmd.integer('amount', 'The amount you want to add.', required: true)
cmd.string('reason', "Reason you're adding money to the wallet. Leave empty for default.", required: false)
end
db.update_balance(user_id, amount, reason)
bot.application_command(:add) do |event|
user_id = event.user.id
amount = event.options['amount']
reason = event.options['reason'] ||= 'transaction'
db.update_balance(user_id, amount, reason)
event.respond(content: "Added: #{amount} to the wallet.\nReason: #{reason}")
end
end
msg = I18n.t('responses.add.success', amount: amount, reason: reason, default: "Added #{amount} (Reason: #{reason})")
event.respond(content: msg)
end
end
end
end

View File

@@ -15,13 +15,13 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
module Commands
module Currency
module Balance
extend self
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
user_id = event.user.id

View File

@@ -13,6 +13,7 @@
#
# 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 'net/http'
require 'uri'
require 'json'

View File

@@ -13,6 +13,7 @@
#
# 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

View File

@@ -50,19 +50,23 @@ class Database
@conn.exec(sql_wallet)
@conn.exec(sql_ledger)
@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."
end
# We pass the 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])
# 2. Check if the user exists
if result.num_tuples.zero?
return 0 # User has no money/row yet
else
# 3. Return the value (don't print it)
return result[0]['amount'].to_i
end
end
@@ -106,4 +110,23 @@ class Database
net: row['net_change'].to_i
}
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

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