A bot created for the Lospec Discord server, with the source made available for community contributions or for learning to make your own similar bot.
This is version 4, rewritten from the ground up to use the latest version of discord.js and node modules.
Not all of the functionality from version 3 has been ported over, and the bots are both run concurrently.
- Fork this project on Github
- Locally clone from your fork.
- In a the project working directory run
npm installto download the required nodeJS modules - (Optional) Configure your local git repo to have the original repository as
upstreamso you can directly pull from it
Make sure you have a discord server set-up where you have admin permissions before following these steps
- Enter the Discord Developer Portal and create a
New Application. This will be your testing application. - With the Application created, select the
Bottab from the left panel. ChooseAdd bot. - Under the username, there will be a section labelled
token, copy this to your clipboard - Create a file called
.envon the root of the project - Add
DISCORD_BOT_TOKEN=to the file, then paste the token afterwards and save
If you have access to a mongodb database, you can set it up to match the production database, or you instead use the local datastore option which just uses .json files.
- Create a MongoDB database
- Download your certificate and save it to
ca-certificate.crt - Add
MONGO_URI=to the .env file with your connection string, and append&tlsCAFile=ca-certificate.crtto the end - Add
DB=LospecBotV4to the .env file - Run the bot and it will automatically create the necessary tables and documents and the appropriate values
- Edit the documents to fill in any blank values
- Reboot the bot
Please note, if you update these documents manually by editing them, you must reboot the bot immediately for the changes to be recognizes. The data is stored in memory and overwrites the document whenever it's changed.
If you don't have access to a mongodb database, or find it too confusing to set up (understandably), you can just use the local option:
- Add
LOCAL_DATA_STORAGE=trueto the .env file - Run the bot, and it will automatically create a
./_datafolder and the necessary .json files - Edit the documents to fill in any blank values
- Reboot the bot
Please note, if you update these files manually by editing them, you must reboot the bot immediately for the changes to be recognizes. The data is stored in memory and overwrites the file whenever it's changed.
To run the bot, run the command npm start from the command line from the project root.
Everything the bot needs to be told — channel ids, prices, timings — lives in
the data stores rather than in code, and admins can read and change all of it
from discord with /config. The command is restricted to administrators, and
does not show up at all for anybody else.
| Command | |
|---|---|
/config list <store> |
everything in a store, with its current value |
/config get <store> <key> |
one value, in full |
/config set <store> <key> <value> |
change a value, or add a new one |
/config clear <store> <key> |
empty a value, as though it had never been set |
/config set guesses the type of what you type: 16 is stored as a number,
true as a boolean, [1,2] and {"a":1} as JSON, and anything else as text.
Long runs of digits are always kept as text, because discord ids are too big
for a javascript number to hold reliably. Use the type option to say
explicitly which you meant.
Changes take effect immediately — the stores are held in memory and the command updates that copy as well as the database, so there is nothing to restart. That is only true for changes made through the command: editing the database directly still needs a reboot, because the bot will otherwise overwrite your edit from memory the next time it writes.
Set statusThreadId in the config store to a thread id and the bot posts
there every time it starts, saying how the previous run ended — restarted by
somebody, shut down cleanly, or killed without ever getting to tidy up.
/config set store:config key:statusThreadId value:<the thread id>
If it crashes it posts there too, with the error and the last few hundred lines of console output attached as a file. Deaths that never reach a handler at all, like running out of memory, cannot post anything themselves — those are noticed by the next run instead, and reported when it comes back up.
/restart restarts the bot deliberately, and needs the Manage Server
permission. It relies on something outside the bot bringing the process back:
pm2, systemd, docker, whatever is running it.
Anything the bot can hold and hand over can be auctioned. An auction is posted as an embed with a Place Bid button, runs for a few days, and then charges the winner and pays the seller. Bids are only checked against your balance when you make them - the winner pays at the end, and if they cannot, it falls to the next highest bidder.
The bidding, the money, the messages and the timers live in util/auctions.js
and are the same for every kind. What is actually being sold is a handler
module per kind, listed in TYPE_MODULES at the top of that file:
| Kind | Handler | What is sold |
|---|---|---|
lft |
util/auction-lft.js |
a Lospec Funky Thingy, out of your inventory |
role |
util/auction-role.js |
a discord role, off your account |
custom |
util/auction-custom.js |
whatever a user says it is |
To add another kind, write a module with a default export implementing as much
of the handler contract as it needs - documented above TYPE_MODULES - and add
it to that list. Nothing else has to change.
An auction is run from its own post: Place Bid is on it, and that is the whole of it. Once something is up it cannot be called off - it runs its course and either sells or comes back. There is nothing to list either, because the auctions are the posts in the thread.
Commands only exist where there is no post to put a button on:
| Command | |
|---|---|
/auction |
auction something of your own, from your post in the auction forum |
/role auction |
put a role you have up for auction |
/role give |
hand a role straight to somebody, free |
/role list |
the tradeable roles, and who has them |
/role tradeable |
admin: say whether a role can be traded at all |
/lft auction |
put one of your LFTs up in the marketplace |
Only roles a moderator has marked tradeable can be bought and sold, and each of them belongs to one person at a time. Putting one up takes it off you immediately and gives it to whoever wins; if nobody bids, you get it back.
/role tradeable role:@Cool Person tradeable:True
The bot needs the Manage Roles permission, and one of its own roles has to sit
above the role in the list, or it cannot hand it out - /role tradeable says so
rather than accepting a role it cannot move. Roles discord manages itself, like
the booster role or a bot's own role, can never be traded.
A tradeable role that nobody holds goes up for auction by the treasury at one pikzel. That covers the role of somebody who leaves the server - which the bot notices as they go - as well as one that has just been marked tradeable, or one whose owner left while the bot was down. The last of those is found by a sweep that runs every half hour, because working out who holds a role means asking discord for the whole member list.
Anybody can auction anything they like from a post they started in the auction forum:
/auction title:A commission starting_bid:50
The bot only handles the pikzels: it takes the bids, charges the winner and pays
the seller. Sending whatever was sold is between the two of them - the bot
says so on the auction and in the thread when it closes. How many auctions one
person may have running at once is customMaxOpenPerUser.
| Store | Key | |
|---|---|---|
lft-data |
marketplaceThreadId |
the thread LFT auctions are posted in |
auction-data |
roleAuctionThreadId |
the thread role auctions are posted in |
auction-data |
customAuctionForumId |
the forum users may auction from, or a list of them |
auction-data |
tradeableRoles |
which roles can be traded - set by /role tradeable, not by hand |
Auctions of a kind whose thread has not been set up are simply never posted, so
each kind can be turned on by itself. The rest of auction-data - how long
bidding runs for, minimum bids, how many auctions one person may have going,
whether unheld roles are relisted, how often to sweep - is listed with its
defaults at the top of util/auction-config.js, and every value can be changed
with /config set store:auction-data.
How to expand the bot with new functionality.
Create a command that users can trigger
Add a file to the ./commands folder with the following exports:
config- an object containing the JSON configuration for a commandexecute- an async function that is called when the function is run (passes interaction as first argument)
To create a command with subcommands (e.g. /parent sub), create a folder inside the ./commands directory (e.g. ./commands/parent). Then, create a JavaScript file for each subcommand (e.g. ./commands/parent/sub.js).
Each subcommand file should have a default export which is an async function that is called when the subcommand is run. This function will receive the interaction object as its first argument.
The parent command (e.g. parent in /parent sub) should have a config export in a file named after the parent command directly in the ./commands folder (e.g. ./commands/parent.js). This file does not need an execute export if all functionality is handled by subcommands.
Respond to a message that matches a filter
Add a file to the ./responses folder with the following exports:
filter- an async function that checks if the message should trigger a responseexecute- an async function that is called when the filter matches
Add an autocomplete that provides suggestions for command options.
Add a file to the ./autocompletes folder with a default export async function that takes the interaction as its first argument. The function should return an array of choices, either as strings or as objects with { name, value } properties. The loader will handle formatting and calling interaction.respond() with the results (up to 25 entries).
Autocompletes are loaded automatically from the autocompletes folder, similar to commands. You can use the same autocomplete in multiple commands by referencing its name.
To use an autocomplete in a command, set the autocomplete property to true in the command option config, and make sure there is a corresponding autocomplete where the file name matches the option name.
To run the bot, you must have set up one of the two data storage options, explained above. Both options have identical APIs.
First you must import the appropriate document from the data module:
import {CONFIG} from '../data.js';
Modules that use a lot of properties should be set up with their own data store, which is defined in the data module:
export const MYDATAMODULENAME = new Data('my-data-module-slug');
- .get(
<string>key ) - Get the value associated with the provided key - .set(
<string>key,<any>value ) - Set the value of the provided key to the provided value - .keys() - List the names of every value in the data store
- .assert(
<string>key,<bool>required [optional] ) (async) - Ensure a value exists in the data store, and if not, create a blank value and throw an error (unless required is set to false). Must be awaited. This gives the developer a place to enter the value manually. This should only be used in the top level of a file, so commands will not be loaded if the assertion fails.