Compare commits
34 Commits
9f333f91fd
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| 5ca1bf798e | |||
| 24dbb16ad7 | |||
| e037333da1 | |||
| ffa982eb74 | |||
| 6c0c15d3fa | |||
| d8fc36bffb | |||
| f516f0cf00 | |||
| 1253471fc3 | |||
| 77d992720d | |||
| eed6a9d9f6 | |||
| 9dc1cf10c7 | |||
| 70e21fd118 | |||
| 4c06e3fbbd | |||
| 28529158e5 | |||
| 13dbb86d38 | |||
| 504773b1ca | |||
| 659bd70c75 | |||
| fc3c4787fe | |||
| 1035b3d0ab | |||
| 98c854929d | |||
| f5ec2f38fc | |||
| ba22833413 | |||
| 1365491c13 | |||
| 741762856d | |||
| 4a535d4080 | |||
| d5e727c358 | |||
| d824afc5da | |||
| f841f6ec95 | |||
| 689079ce21 | |||
| d08af2f294 | |||
| 4586295e62 | |||
| a923fce12e | |||
| 6588d68cc4 | |||
| 86e1af1a66 |
45
.gitea/workflows/compile.yml
Normal file
45
.gitea/workflows/compile.yml
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: Compile
|
||||||
|
run-name: Compiling software (Ubuntu)
|
||||||
|
on: [push]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Compile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
services:
|
||||||
|
postgres:
|
||||||
|
image: postgres:16
|
||||||
|
env:
|
||||||
|
POSTGRES_DB: fgbot_db
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: test_password
|
||||||
|
ports:
|
||||||
|
- 5432:5432
|
||||||
|
options: >-
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Install Ruby and Bundler
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y ruby-full libsodium-dev libpq-dev
|
||||||
|
gem install bundler
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: bundle install
|
||||||
|
|
||||||
|
- name: Execute program
|
||||||
|
env:
|
||||||
|
BOT_TOKEN: ${{ secrets.TOKEN }}
|
||||||
|
TEST_SERVER_ID: ${{ secrets.SERVER_ID }}
|
||||||
|
DB_HOST: 127.0.0.1
|
||||||
|
DB_USER: postgres
|
||||||
|
DB_PASS: test_password
|
||||||
|
DB_NAME: fgbot_db
|
||||||
|
run: bundle exec ruby main.rb
|
||||||
3
Gemfile
3
Gemfile
@@ -3,4 +3,5 @@ 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 'i18n'
|
||||||
@@ -17,6 +17,7 @@ GEM
|
|||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
|
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)
|
||||||
@@ -34,6 +35,8 @@ GEM
|
|||||||
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)
|
||||||
mime-types (3.7.0)
|
mime-types (3.7.0)
|
||||||
logger
|
logger
|
||||||
@@ -78,10 +81,12 @@ PLATFORMS
|
|||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
discordrb!
|
discordrb!
|
||||||
dotenv
|
dotenv
|
||||||
|
i18n
|
||||||
pg
|
pg
|
||||||
|
|
||||||
CHECKSUMS
|
CHECKSUMS
|
||||||
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
|
||||||
|
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
|
||||||
@@ -100,6 +105,7 @@ CHECKSUMS
|
|||||||
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
|
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
|
||||||
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
|
||||||
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
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -1,12 +1,12 @@
|
|||||||
# FrugalityBot
|
# FrugalityBot
|
||||||
|
|
||||||
Frugality is a Discord Bot written in Ruby using `discordrb` and PostgreSQL. Its usage is meant to track your own finances, with monthly reports. Work in progress.
|
Frugality is a Discord Bot written in Ruby using `discordrb` and PostgreSQL. Its usage is meant to track your own finances, with monthly and/or yearly reports. Work in progress.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
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 user data.
|
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:
|
||||||
@@ -46,8 +46,8 @@ The bot requires a PostgreSQL database.
|
|||||||
createdb database
|
createdb database
|
||||||
```
|
```
|
||||||
**The database name is also specified [here](https://git.thenight.club/csxkdv/FrugalityBot/src/branch/main/src/database.rb). Ensure it's the same as the one you'll use.**
|
**The database name is also specified [here](https://git.thenight.club/csxkdv/FrugalityBot/src/branch/main/src/database.rb). Ensure it's the same as the one you'll use.**
|
||||||
|
|
||||||
*Note: The bot will automatically create the necessary tables (like `total_money`) the first time it connects. If you want new tables, update the database file as desired.*
|
*Note: The bot will automatically create the necessary tables (like `wallets` and `transactions`) the first time it connects. If you want new tables, update the database file as desired.*
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
@@ -59,13 +59,17 @@ The bot requires a PostgreSQL database.
|
|||||||
2. Add your secrets to the `.env` file:
|
2. Add your secrets to the `.env` file:
|
||||||
```env
|
```env
|
||||||
BOT_TOKEN=your_discord_bot_token_here
|
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 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:
|
||||||
```bash
|
```bash
|
||||||
bundle exec ruby main.rb
|
bundle exec ruby main.rb
|
||||||
```
|
```
|
||||||
|
Wait for the bot to connect, and that's it, you can start using it.
|
||||||
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
This project is licensed under the [GNU General Public License v3.0](COPYING).
|
This project is licensed under the [GNU General Public License v3.0](https://spdx.org/licenses/GPL-3.0-or-later.html).
|
||||||
53
src/bot.rb
53
src/bot.rb
@@ -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
|
||||||
|
end
|
||||||
comm_files.each do |file|
|
|
||||||
require file # We import the file
|
Commands.constants.each do |const|
|
||||||
|
cmd = Commands.const_get(const)
|
||||||
# We convert filename to module name
|
|
||||||
# This mean that 'echo.rb' turns into 'Echo'
|
if cmd.is_a?(Module) && cmd.respond_to?(:register)
|
||||||
# 'server_info' would turn into 'ServerInfo'
|
cmd.register(@bot, @db)
|
||||||
filename = File.basename(file, '.rb')
|
puts "Loaded command: #{const}"
|
||||||
module_name = filename.split('_').map(&:capitalize).join
|
sleep(1.5)
|
||||||
|
end
|
||||||
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
|
end
|
||||||
puts "Commands loaded."
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def setup_events
|
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)
|
||||||
|
|||||||
53
src/commands/add.rb
Normal file
53
src/commands/add.rb
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# 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 Commands
|
||||||
|
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'
|
||||||
|
|
||||||
|
db.update_balance(user_id, amount, reason)
|
||||||
|
|
||||||
|
msg = I18n.t('responses.add.success', amount: amount, reason: reason, default: "Added #{amount} (Reason: #{reason})")
|
||||||
|
event.respond(content: msg)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -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
|
||||||
|
|
||||||
88
src/commands/statement.rb
Normal file
88
src/commands/statement.rb
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# 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 'net/http'
|
||||||
|
require 'uri'
|
||||||
|
require 'json'
|
||||||
|
|
||||||
|
module Commands
|
||||||
|
module Statement
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def register(bot, db)
|
||||||
|
bot.register_application_command(:statement, "Get a report of your incomes and expenses", server_id: ENV['TEST_SERVER_ID']) do |cmd|
|
||||||
|
cmd.string('period', 'Select time range', required: true, choices: {
|
||||||
|
'Last Month' => '1 month',
|
||||||
|
'Last Year' => '1 year'
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
|
bot.application_command(:statement) do |event|
|
||||||
|
event.defer
|
||||||
|
|
||||||
|
user_id = event.user.id
|
||||||
|
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)
|
||||||
|
|
||||||
|
chart_config = {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: ['Income', 'Expenses'],
|
||||||
|
datasets: [{
|
||||||
|
data: [stats[:income], stats[:expenses].abs],
|
||||||
|
backgroundColor: ['#9AF5AA', '#F55D5D'],
|
||||||
|
borderWidth: 1
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
plugins: {
|
||||||
|
doughnutlabel: {
|
||||||
|
labels: [
|
||||||
|
{ text: stats[:net].to_s, font: { size: 20}},
|
||||||
|
{ text: 'Net', font: { size: 10 }}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded_config = URI.encode_www_form_component(chart_config.to_json)
|
||||||
|
chart_url = "https://quickchart.io/chart?c=#{encoded_config}&w=500&h=300&bkg=rgb(255,255,255)"
|
||||||
|
|
||||||
|
color_generated = ("0x" + Random.bytes(3).unpack1('H*')).to_i(16)
|
||||||
|
|
||||||
|
embed_generated = {
|
||||||
|
title: "Financial Statement",
|
||||||
|
description: "Report generated for **#{event.user.name}** (#{period})",
|
||||||
|
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." }
|
||||||
|
}
|
||||||
|
|
||||||
|
event.edit_response(embeds: [embed_generated])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
52
src/commands/subtract.rb
Normal file
52
src/commands/subtract.rb
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
# 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/>.
|
||||||
|
|
||||||
|
module Commands
|
||||||
|
module Subtract
|
||||||
|
extend self
|
||||||
|
|
||||||
|
def register(bot, db)
|
||||||
|
bot.register_application_command(:subtract, 'Take money from the wallet', server_id: ENV['TEST_SERVER_ID']) do |cmd|
|
||||||
|
cmd.integer('amount', "The amount you're spending.", required: true)
|
||||||
|
cmd.string('reason', "Reason you're spending money. Leave empty for default.", required: false)
|
||||||
|
end
|
||||||
|
|
||||||
|
bot.application_command(:subtract) do |event|
|
||||||
|
amount = event.options['amount'].to_i
|
||||||
|
reason = event.options['reason'] ||= 'expense'
|
||||||
|
|
||||||
|
if amount <= 0
|
||||||
|
event.respond(content: "You either have 0, or you can't spend negative money.", ephemeral: true)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
user_id = event.user.id
|
||||||
|
|
||||||
|
current_balance = db.get_currency(user_id)
|
||||||
|
|
||||||
|
if current_balance < amount
|
||||||
|
event.respond(content: "You can't buy that, you have **#{current_balance}** coins.", ephemeral: true)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
db.update_balance(user_id, -amount, reason)
|
||||||
|
|
||||||
|
new_balance = current_balance - amount
|
||||||
|
event.respond(content: "You spent **#{amount}** coins.\nReason: #{reason}\nYou now have: #{new_balance}")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
module Commands
|
module Commands
|
||||||
module Echo
|
module Echo
|
||||||
extend self
|
extend self
|
||||||
|
|
||||||
def register(bot, _db)
|
def register(bot, _db)
|
||||||
bot.register_application_command(:echo, 'Repeats what you say', server_id: ENV['TEST_SERVER_ID']) do |cmd|
|
bot.register_application_command(:echo, 'Repeats what you say', server_id: ENV['TEST_SERVER_ID']) do |cmd|
|
||||||
cmd.string('message', 'The text you want the bot to repeat', required: true)
|
cmd.string('message', 'The text you want the bot to repeat', required: true)
|
||||||
45
src/commands/testing/embed.rb
Normal file
45
src/commands/testing/embed.rb
Normal 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
|
||||||
102
src/database.rb
102
src/database.rb
@@ -19,34 +19,118 @@ require 'pg'
|
|||||||
class Database
|
class Database
|
||||||
def initialize
|
def initialize
|
||||||
# Connect once when the bot starts
|
# Connect once when the bot starts
|
||||||
@conn = PG.connect(dbname: 'frugality_database')
|
@conn = PG.connect(
|
||||||
|
host: ENV['DB_HOST'] || 'localhost',
|
||||||
|
dbname: ENV['DB_NAME'] || 'fgbot_db',
|
||||||
|
user: ENV['DB_USER'] || 'postgres'
|
||||||
|
)
|
||||||
|
|
||||||
init_tables
|
init_tables
|
||||||
end
|
end
|
||||||
|
|
||||||
def init_tables
|
def init_tables
|
||||||
sql = <<~SQL
|
sql_wallet = <<~SQL
|
||||||
CREATE TABLE IF NOT EXISTS total_money (
|
CREATE TABLE IF NOT EXISTS wallets (
|
||||||
user_id BIGINT PRIMARY KEY,
|
user_id BIGINT PRIMARY KEY,
|
||||||
amount BIGINT DEFAULT 0
|
amount BIGINT DEFAULT 0
|
||||||
);
|
);
|
||||||
SQL
|
SQL
|
||||||
|
|
||||||
@conn.exec(sql)
|
sql_ledger = <<~SQL
|
||||||
|
CREATE TABLE IF NOT EXISTS transactions (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
user_id BIGINT NOT NULL,
|
||||||
|
amount BIGINT NOT NULL,
|
||||||
|
reason VARCHAR(50),
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
sql_index = <<~SQL
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_user_history
|
||||||
|
ON transactions(user_id, created_at);
|
||||||
|
SQL
|
||||||
|
|
||||||
|
@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."
|
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 total_money 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
|
||||||
|
|
||||||
|
def update_balance(user_id, amount, reason = "transaction")
|
||||||
|
@conn.transaction do
|
||||||
|
@conn.exec_params(
|
||||||
|
"INSERT INTO transactions (user_id, amount, reason) VALUES ($1, $2, $3)",
|
||||||
|
[user_id, amount, reason]
|
||||||
|
)
|
||||||
|
|
||||||
|
# We update the user's wallet
|
||||||
|
sql_update = <<~SQL
|
||||||
|
INSERT INTO wallets (user_id, amount)
|
||||||
|
VALUES ($1, $2)
|
||||||
|
ON CONFLICT (user_id)
|
||||||
|
DO UPDATE SET amount = wallets.amount + $2
|
||||||
|
SQL
|
||||||
|
|
||||||
|
@conn.exec_params(sql_update, [user_id, amount])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fetch_report(user_id, interval_string)
|
||||||
|
sql = <<~SQL
|
||||||
|
SELECT
|
||||||
|
SUM(CASE WHEN amount > 0 THEN amount ELSE 0 END) as income,
|
||||||
|
SUM(CASE WHEN amount < 0 THEN amount ELSE 0 END) as expenses,
|
||||||
|
SUM(amount) AS net_change
|
||||||
|
FROM transactions
|
||||||
|
WHERE user_id = $1
|
||||||
|
AND created_at >= NOW() - $2::INTERVAL
|
||||||
|
SQL
|
||||||
|
|
||||||
|
result = @conn.exec_params(sql, [user_id, interval_string])
|
||||||
|
row = result[0]
|
||||||
|
|
||||||
|
{
|
||||||
|
income: row['income'].to_i,
|
||||||
|
expenses: row['expenses'].to_i,
|
||||||
|
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
|
end
|
||||||
19
src/locales/en.yml
Normal file
19
src/locales/en.yml
Normal 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
19
src/locales/es.yml
Normal 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."
|
||||||
41
src/utils/locales_helper.rb
Normal file
41
src/utils/locales_helper.rb
Normal 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
|
||||||
Reference in New Issue
Block a user