Migrate to JavaScript
parent
aa2cfb9d22
commit
e58391eefd
|
@ -1,10 +1,4 @@
|
|||
# Ignore everything
|
||||
*
|
||||
|
||||
# Except these
|
||||
!.gitignore
|
||||
!minetestbot.lua
|
||||
!README.md
|
||||
!relay.lua
|
||||
!settings.example
|
||||
!util.lua
|
||||
config.json
|
||||
*-lock.json
|
||||
node_modules/
|
||||
legacy/
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
MIT Copyright 2019 GreenDimond
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included
|
||||
in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
15
README.md
15
README.md
|
@ -1,16 +1,19 @@
|
|||
# MinetestBot for Discord #
|
||||
_MinetestBot and it's author(s) are in no way affiliated with the IRC MinetestBot._
|
||||
|
||||
Discordia and Luvit are required to run this bot.
|
||||
See the [Discordia README](https://github.com/SinisterRectus/Discordia/blob/master/README.md) for instructions on insalling Discordia and Luvit.
|
||||
Uses NodeJS and `discord.js`.
|
||||
|
||||
To run the bot, use `/path/to/luvit/executable/minetestbot.lua`.
|
||||
To use the bot, run `node .` in the bot directory.
|
||||
|
||||
A `settings.lua` file is required for the bot to run. See `settings.example`.
|
||||
MinetestBot demands a ~~sacrifice~~ `config.json`. See `config.example`.
|
||||
|
||||
### Todo (PRs welcome) ###
|
||||
* Vote starter
|
||||
* Serverlist searcher/parser
|
||||
* Metric/US value conversion command
|
||||
* `minetest.conf.example` search
|
||||
* Define
|
||||
* Word definition
|
||||
* Forum search
|
||||
|
||||
Might be missing `;` everywhere.
|
||||
|
||||
This used to be written in Lua, but Luvit is garbage.
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
const {color} = require("../config.js");
|
||||
const request = require("request");
|
||||
|
||||
module.exports = {
|
||||
name: "cdb",
|
||||
aliases: ["contentdb", "mod", "modsearch", "search"],
|
||||
usage: "[search term]",
|
||||
description: "Search the ContentDB",
|
||||
execute: function(message, args) {
|
||||
if (!args.length) {
|
||||
const embed = {
|
||||
url: "https://content.minetest.net/",
|
||||
title: "**ContentDB**",
|
||||
description: "Minetest's official content repository.",
|
||||
color: color,
|
||||
thumbnail: {
|
||||
url: "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Minetest-logo.svg/1024px-Minetest-logo.svg.png",
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Usage:",
|
||||
value: "`<search term>`"
|
||||
}
|
||||
],
|
||||
};
|
||||
message.channel.send({embed: embed});
|
||||
} else {
|
||||
const term = args.join(" ");
|
||||
request({
|
||||
url: `https://content.minetest.net/api/packages/?q=${term}&lucky=1`,
|
||||
json: true,
|
||||
}, function(err, res, body) {
|
||||
if (!body.length) {
|
||||
const embed = {
|
||||
title: `Could not find any packages related to "${term}".`,
|
||||
color: color,
|
||||
};
|
||||
message.channel.send({embed: embed});
|
||||
} else {
|
||||
const meta = body[0];
|
||||
request({
|
||||
url: `https://content.minetest.net/api/packages/${meta.author}/${meta.name}/`,
|
||||
json: true,
|
||||
}, function(err, res, pkg) {
|
||||
let desc = `${pkg.short_description}`;
|
||||
let info = [];
|
||||
if (pkg.forums) info.push(`[Forum thread](https://forum.minetest.net/viewtopic.php?t=${pkg.forums})`);
|
||||
if (pkg.repo) info.push(`[Git Repo](${pkg.repo})`);
|
||||
const embed = {
|
||||
url: encodeURI(`https://content.minetest.net/packages/${meta.author}/${meta.name}/`),
|
||||
title: `**${pkg.title}**`,
|
||||
description: `By ${pkg.author}`,
|
||||
color: color,
|
||||
image: {
|
||||
url: pkg.thumbnail
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Description:",
|
||||
value: `${desc}\n${info.join(" | ")}`
|
||||
}
|
||||
]
|
||||
};
|
||||
message.channel.send({embed: embed});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
const {color, version} = require("../config.js")
|
||||
const minetest_logo = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Minetest-logo.svg/1024px-Minetest-logo.svg.png";
|
||||
const confURL = `https://github.com/minetest/minetest/blob/${version}/minetest.conf.example`;
|
||||
const rawURL = `https://raw.githubusercontent.com/minetest/minetest/${version}/minetest.conf.example`;
|
||||
const pageSize = 6;
|
||||
const pages = require("../pages.js");
|
||||
|
||||
module.exports = {
|
||||
name: "conf",
|
||||
aliases: ["mtconf"],
|
||||
usage: "<search term>",
|
||||
description: "Search minetest.conf.example",
|
||||
execute: async function(message, args) {
|
||||
if (!args.length) {
|
||||
const embed = {
|
||||
title: "Minetest Configuration",
|
||||
thumbnail: {
|
||||
url: minetest_logo,
|
||||
},
|
||||
color: color,
|
||||
fields: [
|
||||
{
|
||||
name: "minetest.conf.example (stable)",
|
||||
value: confURL,
|
||||
},
|
||||
{
|
||||
name: "minetest.conf.example (unstable)",
|
||||
value: "https://github.com/minetest/minetest/blob/master/minetest.conf.example"
|
||||
}
|
||||
]
|
||||
};
|
||||
const msg = await message.channel.send({embed: embed});
|
||||
pages.addControls(msg, false);
|
||||
} else {
|
||||
const term = args.join(" ");
|
||||
pages.getPage("conf", message, {
|
||||
url: {
|
||||
search: rawURL,
|
||||
display: confURL
|
||||
},
|
||||
page: 1,
|
||||
pageSize: pageSize,
|
||||
title: "Minetest Configuration",
|
||||
thumbnail: minetest_logo,
|
||||
}, term, async function(embed, results) {
|
||||
let turn = true;
|
||||
if (results.length > 100) turn = false;
|
||||
const msg = await message.channel.send({embed: embed});
|
||||
pages.addControls(msg, turn);
|
||||
});
|
||||
}
|
||||
},
|
||||
page: {
|
||||
execute: function(message, page) {
|
||||
const oldEmbed = message.embeds[0];
|
||||
const term = oldEmbed.description.match(/Results for \[`(.+)`\]/)[1];
|
||||
pages.getPage("conf", message, {
|
||||
url: {
|
||||
search: rawURL,
|
||||
display: confURL
|
||||
},
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
title: "Minetest Configuration",
|
||||
thumbnail: minetest_logo,
|
||||
}, term, function(embed) {
|
||||
embed.footer.icon_url = oldEmbed.footer.iconURL;
|
||||
message.edit({embed: embed});
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
|
@ -0,0 +1,84 @@
|
|||
const {prefix} = require("../config.json");
|
||||
const {color} = require("../config.js");
|
||||
|
||||
module.exports = {
|
||||
name: "help",
|
||||
description: "List available commands or info about a specific command.",
|
||||
aliases: ["commands"],
|
||||
usage: "[command name]",
|
||||
// cooldown: 5,
|
||||
execute(message, args) {
|
||||
const client = message.client;
|
||||
const {commands} = client;
|
||||
|
||||
const fields = [];
|
||||
|
||||
let append = "";
|
||||
|
||||
if (!args.length) {
|
||||
commands.array().forEach((command) => {
|
||||
let desc = "No description.";
|
||||
if (command.description) desc = command.description;
|
||||
|
||||
let aliases = "";
|
||||
if (command.aliases) aliases = ` | Aliases: ${command.aliases.join(", ")}`;
|
||||
|
||||
fields.push({
|
||||
name: `Command: \`${command.name}\``,
|
||||
value: `${desc}${aliases}`
|
||||
})
|
||||
})
|
||||
|
||||
append = ` | See ${prefix}help <command> for usage.`;
|
||||
} else {
|
||||
const name = args[0].toLowerCase();
|
||||
const command = commands.get(name) || commands.find(c => c.aliases && c.aliases.includes(name));
|
||||
|
||||
if (!command) {
|
||||
message.channel.send({embed: {
|
||||
color: color,
|
||||
title: `Command "${name}" does not exist.`,
|
||||
timestamp: new Date(),
|
||||
footer: {
|
||||
text: `Use ${prefix}help to list all commands.`
|
||||
},
|
||||
}});
|
||||
return;
|
||||
}
|
||||
|
||||
let desc = "No description.";
|
||||
if (command.description) desc = command.description;
|
||||
|
||||
let aliases = "";
|
||||
if (command.aliases) aliases = ` | Aliases: ${command.aliases.join(", ")}`;
|
||||
|
||||
let usage = "";
|
||||
if (command.usage) usage = `\nUsage: \`${command.usage}\``;
|
||||
fields.push({
|
||||
name: `Command: \`${command.name}\``,
|
||||
value: `${desc}${aliases}${usage}`
|
||||
})
|
||||
}
|
||||
|
||||
const embed = {
|
||||
color: color,
|
||||
title: `${client.user.username} Commands:`,
|
||||
thumbnail: {
|
||||
url: client.user.avatarURL,
|
||||
},
|
||||
fields: fields,
|
||||
timestamp: new Date(),
|
||||
footer: {
|
||||
text: `Prefix: ${prefix}${append}`
|
||||
},
|
||||
};
|
||||
|
||||
return message.author.send({embed: embed}).then(() => {
|
||||
if (message.channel.type === "dm") return;
|
||||
message.reply("I\'ve sent you a DM with all my commands.");
|
||||
}).catch(error => {
|
||||
console.error(`Could not send help DM to ${message.author.tag}.\n`, error);
|
||||
message.reply("help DM couldn't be sent, do you have DMs disabled?");
|
||||
});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,64 @@
|
|||
const {color} = require("../config.js");
|
||||
|
||||
function pluralize(time, period) {
|
||||
const msg = `${time.toString()} ${period}`;
|
||||
if (time > 1) {
|
||||
return `${msg}s, `;
|
||||
} else if (time == 0) {
|
||||
return "";
|
||||
}
|
||||
return `${msg}, `;
|
||||
}
|
||||
|
||||
function duration(ms) {
|
||||
let sec = (ms / 1000) | 0;
|
||||
|
||||
let min = (sec / 60) | 0;
|
||||
sec -= min * 60;
|
||||
|
||||
let hrs = (min / 60) | 0;
|
||||
min -= hrs * 60;
|
||||
|
||||
let day = (hrs / 24) | 0;
|
||||
hrs -= day * 24;
|
||||
|
||||
let wks = (day / 7) | 0;
|
||||
day -= wks * 7;
|
||||
|
||||
return `${pluralize(wks, "week")}${pluralize(day, "day")}${pluralize(hrs, "hour")}${pluralize(min, "minute")}${pluralize(sec, "second").slice(0, -2)}.`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: "info",
|
||||
aliases: ["about"],
|
||||
description: "Bot info.",
|
||||
execute(message) {
|
||||
const client = message.client;
|
||||
const creator = client.users.get("286032516467654656");
|
||||
const embed = {
|
||||
color: color,
|
||||
title: `${client.user.username} Info`,
|
||||
description: "Open-source, JavaScript-powered, Discord bot providing useful Minetest features. Consider [donating](https://www.patreon.com/GreenXenith/).",
|
||||
thumbnail: {
|
||||
url: client.user.avatarURL,
|
||||
},
|
||||
fields: [
|
||||
{
|
||||
name: "Sauce",
|
||||
value: "<https://github.com/GreenXenith/minetestbot>"
|
||||
},
|
||||
{
|
||||
name: "Uptime",
|
||||
value: duration(client.uptime)
|
||||
},
|
||||
],
|
||||
timestamp: new Date(),
|
||||
footer: {
|
||||
text: `Created by ${creator.tag}`,
|
||||
icon_url: creator.avatarURL,
|
||||
},
|
||||
};
|
||||
|
||||
message.channel.send({embed: embed});
|
||||
},
|
||||
};
|
|
@ -0,0 +1,72 @@
|
|||
const {color} = require("../config.js");
|
||||
|
||||
module.exports = {
|
||||
name: "lmgtfy",
|
||||
usage: "[-x] [-g|y|d|b] <search term>",
|
||||
description: "Let Me Google That For You.",
|
||||
execute: function(message, args) {
|
||||
let iie = false;
|
||||
let engine = "g";
|
||||
let footer = "";
|
||||
|
||||
const valid = ["g", "d", "b", "y"];
|
||||
const a = new RegExp(/^\-\w$/);
|
||||
|
||||
while (a.test(args[0])) {
|
||||
const arg = args.shift().slice(1);
|
||||
if (valid.includes(arg)) {
|
||||
engine = arg;
|
||||
} else if (arg === "x") {
|
||||
iie = true;
|
||||
footer = "Internet Explainer";
|
||||
}
|
||||
}
|
||||
|
||||
const engines = {
|
||||
g: {
|
||||
icon: "https://cdn4.iconfinder.com/data/icons/new-google-logo-2015/400/new-google-favicon-512.png",
|
||||
name: "Google",
|
||||
color: "4081EC",
|
||||
},
|
||||
y: {
|
||||
icon: "https://cdn1.iconfinder.com/data/icons/smallicons-logotypes/32/yahoo-512.png",
|
||||
name: "Yahoo",
|
||||
color: "770094",
|
||||
},
|
||||
b: {
|
||||
icon: "https://cdn.icon-icons.com/icons2/1195/PNG/512/1490889706-bing_82538.png",
|
||||
name: "Bing",
|
||||
color: "ECB726",
|
||||
},
|
||||
d: {
|
||||
icon: "https://cdn.icon-icons.com/icons2/844/PNG/512/DuckDuckGo_icon-icons.com_67089.png",
|
||||
name: "DuckDuckGo",
|
||||
color: "D75531",
|
||||
},
|
||||
};
|
||||
|
||||
const term = args.join(" ");
|
||||
let embed = {};
|
||||
|
||||
if (term === "") {
|
||||
embed = {
|
||||
title: "Empty search term.",
|
||||
color: color
|
||||
};
|
||||
} else {
|
||||
embed = {
|
||||
title: `${engines[engine].name} Search:`,
|
||||
thumbnail: {
|
||||
url: engines[engine].icon,
|
||||
},
|
||||
description: `[Search for "${term}"](http://lmgtfy.com/?s=${engine}&iie=${iie}&q=${encodeURI(term)}).`,
|
||||
color: parseInt(engines[engine].color, 16),
|
||||
footer: {
|
||||
text: footer
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
message.channel.send({embed: embed});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,81 @@
|
|||
const {color, version} = require("../config.js")
|
||||
const minetest_logo = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Minetest-logo.svg/1024px-Minetest-logo.svg.png";
|
||||
const apiURL = `https://github.com/minetest/minetest/blob/${version}/doc/lua_api.txt`;
|
||||
const rawURL = `https://raw.githubusercontent.com/minetest/minetest/${version}/doc/lua_api.txt`;
|
||||
const pageSize = 6;
|
||||
const pages = require("../pages.js");
|
||||
|
||||
module.exports = {
|
||||
name: "lua_api",
|
||||
aliases: ["api", "rtfm", "docs", "doc"],
|
||||
usage: "<search term>",
|
||||
description: "Get Lua API links or search the Lua API",
|
||||
execute: async function(message, args) {
|
||||
if (!args.length) {
|
||||
const embed = {
|
||||
title: "Lua API",
|
||||
thumbnail: {
|
||||
url: minetest_logo,
|
||||
},
|
||||
description: "Minetest Lua API Documentation",
|
||||
color: color,
|
||||
fields: [
|
||||
{
|
||||
name: `lua_api.txt (stable, ${version})`,
|
||||
value: `Lua API in a text file (use CTRL+F). Located [here](${apiURL}).`
|
||||
},
|
||||
{
|
||||
name: "lua_api.txt (unstable)",
|
||||
value: "Unstable Lua API in a text file (use CTRL+F). Located [here](https://github.com/minetest/minetest/blob/master/doc/lua_api.txt)."
|
||||
},
|
||||
{
|
||||
name: "Read the Docs Minetest API",
|
||||
value: "lua_api.txt with very nice formatting. Located [here](http://minetest.gitlab.io/minetest/)."
|
||||
},
|
||||
{
|
||||
name: "lua_api.txt with proper markdown",
|
||||
value: "lua_api.txt but looks a little nicer. Located [here](https://rubenwardy.com/minetest_modding_book/lua_api.html)."
|
||||
},
|
||||
]
|
||||
};
|
||||
const msg = await message.channel.send({embed: embed});
|
||||
pages.addControls(msg, false);
|
||||
} else {
|
||||
const term = args.join(" ");
|
||||
pages.getPage("lua_api", message, {
|
||||
url: {
|
||||
search: rawURL,
|
||||
display: apiURL
|
||||
},
|
||||
page: 1,
|
||||
pageSize: pageSize,
|
||||
title: "Minetest Lua API",
|
||||
thumbnail: minetest_logo,
|
||||
}, term, async function(embed, results) {
|
||||
let turn = true;
|
||||
if (results.length > 100) turn = false;
|
||||
const msg = await message.channel.send({embed: embed});
|
||||
pages.addControls(msg, turn);
|
||||
});
|
||||
}
|
||||
},
|
||||
page: {
|
||||
execute: function(message, page) {
|
||||
const oldEmbed = message.embeds[0];
|
||||
const term = oldEmbed.description.match(/Results for \[`(.+)`\]/)[1];
|
||||
pages.getPage("lua_api", message, {
|
||||
url: {
|
||||
search: rawURL,
|
||||
display: apiURL
|
||||
},
|
||||
page: page,
|
||||
pageSize: pageSize,
|
||||
title: "Minetest Lua API",
|
||||
thumbnail: minetest_logo,
|
||||
}, term, function(embed) {
|
||||
embed.footer.icon_url = oldEmbed.footer.iconURL;
|
||||
message.edit({embed: embed});
|
||||
});
|
||||
},
|
||||
}
|
||||
};
|
|
@ -0,0 +1,190 @@
|
|||
const {prefix} = require("../config.json");
|
||||
const {color, version} = require("../config.js");
|
||||
const minetest_logo = "https://upload.wikimedia.org/wikipedia/commons/thumb/7/73/Minetest-logo.svg/1024px-Minetest-logo.svg.png";
|
||||
const commands = {
|
||||
default: {
|
||||
title: "Helpful Minetest Commands",
|
||||
fields: [
|
||||
{
|
||||
name: "Usage:",
|
||||
value: `\`${prefix}minetest <command>\``
|
||||
},
|
||||
{
|
||||
name: "Avaliable commands:",
|
||||
value: "```\ninstall\ncompile\nabout```"
|
||||
},
|
||||
]
|
||||
},
|
||||
install: {
|
||||
default: {
|
||||
name: "Install Minetest",
|
||||
url: "https://www.minetest.net/downloads/",
|
||||
title: "Downloads for Minetest are located here.",
|
||||
fields: [
|
||||
{
|
||||
name: `Use \`${prefix}minetest install OShere\` for OS-specific instructions.`,
|
||||
value: "```\nlinux\nwindows\nmac\nandroid\nios```"
|
||||
},
|
||||
]
|
||||
},
|
||||
linux: {
|
||||
name: "Install Minetest (Linux)",
|
||||
icon: "http://www.stickpng.com/assets/images/58480e82cef1014c0b5e4927.png",
|
||||
title: "The recommended way to install Minetest on Linux is through your package manager.\nNote: the version shipped by default may be out of date.\nIn which case, you can use a PPA (if applicable), or compiling may be a better option.",
|
||||
fields: [
|
||||
{
|
||||
name: "__For Debian/Ubuntu-based Distributions:__",
|
||||
value: "Open a terminal and run these 3 commands:\n```sudo add-apt-repository ppa:minetestdevs/stable\nsudo apt update\nsudo apt install minetest```"
|
||||
},
|
||||
{
|
||||
name: "__For Arch Distributions:__",
|
||||
value: "Open a terminal and run this command:\n```sudo pacman -S minetest```"
|
||||
},
|
||||
{
|
||||
name: "Again, this will vary depending on your distribution. ",
|
||||
value: `**[Google](https://www.google.com/) is your friend.**\n\nWhile slightly more involved, compiling works on any Linux distribution.\nSee \`${prefix}minetest compile linux\` for details.`
|
||||
},
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
name: "Install Minetest (Windows)",
|
||||
icon: "http://pngimg.com/uploads/windows_logos/windows_logos_PNG25.png",
|
||||
title: "Installing Minetest on Windows is quite simple.",
|
||||
fields: [
|
||||
{
|
||||
name: "__Download:__",
|
||||
value: "Visit https://www.minetest.net/downloads/, navigate to the Windows downloads, and download the proper package for your system."
|
||||
},
|
||||
{
|
||||
name: "__Installation:__",
|
||||
value: "Extract your Minetest folder to the location of your choice.\n\nThe executable is located in `YOUR-DIR-PATH\\minetest\\bin\\`.\n\nCreate a desktop link to the executable."
|
||||
},
|
||||
]
|
||||
},
|
||||
mac: {
|
||||
name: "Install Minetest (MacOS)",
|
||||
icon: "https://upload.wikimedia.org/wikipedia/commons/thumb/b/bb/OS_X_El_Capitan_logo.svg/1024px-OS_X_El_Capitan_logo.svg.png",
|
||||
title: "Installing Minetest on MacOS is about as simple as it gets.",
|
||||
fields: [
|
||||
{
|
||||
name: "__Installation:__",
|
||||
value: "Open a terminal.\n\nMake sure you have [homebrew](https://brew.sh/) installed!\nRun:\n```brew install minetest```"
|
||||
},
|
||||
]
|
||||
},
|
||||
android: {
|
||||
name: "Install Minetest (Android)",
|
||||
icon: "https://cdn.freebiesupply.com/logos/large/2x/android-logo-png-transparent.png",
|
||||
title: "Minetest on mobile devices is not the best experience, but if you really must..",
|
||||
fields: [
|
||||
{
|
||||
name: "__Download:__",
|
||||
value: "Navigate to the Google Play Store.\n\nSearch for and download the [official Minetest app](https://play.google.com/store/apps/details?id=net.minetest.minetest).\n\nOptionally, you may also download [Rubenwardy's Minetest Mods app](https://play.google.com/store/apps/details?id=com.rubenwardy.minetestmodmanager)."
|
||||
},
|
||||
]
|
||||
},
|
||||
ios: {
|
||||
name: "Install Minetest (iOS)",
|
||||
icon: "https://png.icons8.com/color/1600/ios-logo.png",
|
||||
title: "Step one: Switch to an Android device!",
|
||||
fields: [
|
||||
{
|
||||
name: "__Installation:__",
|
||||
value: "No, seriously. There is no official Minetest app on the app store for iOS devices.\nHowever, there are more than enough \"clones\" full of ads and bugs you could try if you really wanted to."
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
compile: {
|
||||
default: {
|
||||
name: "Compile Minetest",
|
||||
url: "https://dev.minetest.net/Compiling_Minetest",
|
||||
title: "Compiling instructions are located here.",
|
||||
fields: [
|
||||
{
|
||||
name: `Use \`${prefix}minetest compile OShere\` for OS-specific instructions.`,
|
||||
value: "```\nlinux\nwindows```"
|
||||
},
|
||||
]
|
||||
},
|
||||
linux: {
|
||||
name: "Compile Minetest (Linux)",
|
||||
icon: "http://www.stickpng.com/assets/images/58480e82cef1014c0b5e4927.png",
|
||||
title: "Compiling on Linux will allow you to view and modify the source code yourself, as well as run multiple Minetest builds.",
|
||||
fields: [
|
||||
{
|
||||
name: "__Compiling:__",
|
||||
value: "Open a terminal.\n\nInstall dependencies. Here's an example for Debian-based and Ubuntu-based distributions:\n```apt install build-essential cmake git libirrlicht-dev libbz2-dev libgettextpo-dev libfreetype6-dev libpng12-dev libjpeg8-dev libxxf86vm-dev libgl1-mesa-dev libsqlite3-dev libogg-dev libvorbis-dev libopenal-dev libhiredis-dev libcurl3-dev```\n\nDownload the engine:```git clone https://github.com/minetest/minetest.git cd minetest/```\n\nDownload Minetest Game:\n```cd games/ git clone https://github.com/minetest/minetest_game.git cd ../```\n\nBuild the game (the make command is set to automatically detect the number of CPU threads to use):\n```cmake . -DENABLE_GETTEXT=1 -DENABLE_FREETYPE=1 -DENABLE_LEVELDB=1 -DENABLE_REDIS=1\nmake -j$(grep -c processor /proc/cpuinfo)```\n\nFor more information, see https://dev.minetest.net/Compiling_Minetest#Compiling_on_GNU.2FLinux."
|
||||
},
|
||||
]
|
||||
},
|
||||
windows: {
|
||||
name: "Compile Minetest (Windows)",
|
||||
icon: "http://pngimg.com/uploads/windows_logos/windows_logos_PNG25.png",
|
||||
title: "Compiling on Windows is not an easy task, and is not going to be covered easily by me.",
|
||||
fields: [
|
||||
{
|
||||
name: "__Compiling:__",
|
||||
value: "Please see https://dev.minetest.net/Compiling_Minetest#Compiling_on_Windows for instructions on compiling Minetest on Windows."
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
about: {
|
||||
default: {
|
||||
name: "About Minetest",
|
||||
icon: minetest_logo,
|
||||
fields: [
|
||||
{
|
||||
name: "Features",
|
||||
value: "https://www.minetest.net/#Features"
|
||||
},
|
||||
{
|
||||
name: "Latest Version",
|
||||
value: `[${version}](https://www.github.com/minetest/minetest/releases/latest)`
|
||||
}
|
||||
]
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
name: "minetest",
|
||||
aliases: ["mt"],
|
||||
description: "General Minetest helpers.",
|
||||
usage: "Use empty command to see options.",
|
||||
execute(message, args) {
|
||||
let content = {};
|
||||
if (!args[0]) {
|
||||
content.title = commands.default.title;
|
||||
content.fields = commands.default.fields;
|
||||
content.name = "Minetest";
|
||||
content.icon = minetest_logo;
|
||||
} else {
|
||||
if (!commands[args[0]]) return;
|
||||
|
||||
if (!args[1]) args[1] = "default";
|
||||
if (!commands[args[0]][args[1]]) return;
|
||||
|
||||
content.url = commands[args[0]][args[1]].url;
|
||||
content.title = commands[args[0]][args[1]].title;
|
||||
content.fields = commands[args[0]][args[1]].fields;
|
||||
content.name = commands[args[0]][args[1]].name;
|
||||
content.icon = commands[args[0]][args[1]].icon;
|
||||
}
|
||||
|
||||
const embed = {
|
||||
url: content.url || "",
|
||||
title: content.title,
|
||||
color: color,
|
||||
author: {
|
||||
name: content.name,
|
||||
icon_url: content.icon
|
||||
},
|
||||
fields: content.fields || []
|
||||
};
|
||||
|
||||
// Send the message
|
||||
message.channel.send({embed: embed});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
const {color} = require("../config.js");
|
||||
const request = require("request");
|
||||
const jsonURL = "https://rubenwardy.com/minetest_modding_book/sitemap.json";
|
||||
const bookURL = "https://rubenwardy.com/minetest_modding_book/en/index.html";
|
||||
const rubenAvatar = "https://avatars0.githubusercontent.com/u/2122943?s=460&v=4.png";
|
||||
|
||||
const pageSize = 6;
|
||||
const pages = require("../pages.js");
|
||||
|
||||
function bookPage(message, page, func) {
|
||||
request({
|
||||
url: jsonURL,
|
||||
json: true,
|
||||
}, async function(err, res, body) {
|
||||
const embed = {
|
||||
title: "Minetest Modding Book",
|
||||
url: bookURL,
|
||||
thumbnail: {
|
||||
url: rubenAvatar,
|
||||
},
|
||||
description: "By Rubenwardy",
|
||||
color: color,
|
||||
footer: pages.pageFooter(message, "modbook", page, Math.ceil(body.length / pageSize)),
|
||||
fields: [],
|
||||
};
|
||||
|
||||
let chapters = [];
|
||||
|
||||
// Get chapters
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const c = body[i];
|
||||
if (c.chapter_number) {
|
||||
chapters.push(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Populate page
|
||||
for (let i = (page - 1) * pageSize; i < (page * pageSize); i++) {
|
||||
const c = chapters[i];
|
||||
if (!c) break;
|
||||
embed.fields.push({
|
||||
name: `**Chapter ${c.chapter_number}: ${c.title}**`,
|
||||
value: `${c.description || ""} [[Open]](${c.loc})`
|
||||
});
|
||||
}
|
||||
|
||||
func(embed);
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: "modbook",
|
||||
aliases: ["book", "mb"],
|
||||
usage: "[search term]",
|
||||
description: "Search Rubenwardy's Modding Book",
|
||||
execute: async function(message, args) {
|
||||
if(!args.length) {
|
||||
bookPage(message, 1, async function(embed) {
|
||||
const msg = await message.channel.send({embed: embed});
|
||||
pages.addControls(msg);
|
||||
});
|
||||
} else {
|
||||
const term = args[0].toLowerCase();
|
||||
request({
|
||||
url: jsonURL,
|
||||
json: true,
|
||||
}, async function(err, res, body) {
|
||||
let chapters = [];
|
||||
let chapter = 0;
|
||||
|
||||
// Get chapters
|
||||
for (let i = 0; i < body.length; i++) {
|
||||
const c = body[i];
|
||||
if (c.chapter_number) {
|
||||
chapters.push(c)
|
||||
}
|
||||
}
|
||||
|
||||
// Test for chapter number
|
||||
if (new RegExp(/^\d+$/).test(term)) {
|
||||
const ch = parseInt(term.match(/^(\d+)$/)[1]);
|
||||
if (chapters[ch - 1]) {
|
||||
chapter = ch;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for term
|
||||
if (chapter === 0) {
|
||||
let results = [];
|
||||
for (let i = 0; i < chapters.length; i++) {
|
||||
const c = chapters[i];
|
||||
results.push([i, 0]);
|
||||
if (c.title.toLowerCase().includes(term)) results[i][1] += 2;
|
||||
if (c.description && c.description.toLowerCase().includes(term)) results[i][1] += 1;
|
||||
}
|
||||
results.sort(function(a, b) {return b[1] - a[1]});
|
||||
const top = results[0];
|
||||
if (top[1] > 0) {
|
||||
chapter = top[0] + 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (chapter != 0) {
|
||||
const c = chapters[chapter - 1];
|
||||
const embed = {
|
||||
title: "Minetest Modding Book",
|
||||
url: bookURL,
|
||||
thumbnail: {
|
||||
url: rubenAvatar,
|
||||
},
|
||||
color: color,
|
||||
fields: [
|
||||
{
|
||||
name: `**Chapter ${c.chapter_number}: ${c.title}**`,
|
||||
value: `${c.description || ""} [[Open]](${c.loc})`
|
||||
}
|
||||
]
|
||||
};
|
||||
message.channel.send({embed: embed})
|
||||
} else {
|
||||
const embed = {
|
||||
title: `Could not find any chapter related to "${term}".`,
|
||||
color: color,
|
||||
};
|
||||
message.channel.send({embed: embed})
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
page: {
|
||||
execute: function(message, page) {
|
||||
const oldEmbed = message.embeds[0]
|
||||
bookPage(message, page, function(embed) {
|
||||
embed.footer.icon_url = oldEmbed.footer.iconURL;
|
||||
message.edit({embed: embed});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
|
@ -0,0 +1,11 @@
|
|||
module.exports = {
|
||||
name: "ping",
|
||||
description: "Ping pong.",
|
||||
execute(message) {
|
||||
const res = ":ping_pong: Pong.";
|
||||
message.channel.send(res + "..").then(msg => {
|
||||
const ping = msg.createdTimestamp - message.createdTimestamp
|
||||
msg.edit(`${res} ${ping}ms.`);
|
||||
})
|
||||
},
|
||||
};
|
|
@ -0,0 +1,19 @@
|
|||
const config = require("./config.json");
|
||||
const request = require("sync-request");
|
||||
|
||||
let version = "";
|
||||
|
||||
console.log("Loading configurations...");
|
||||
const res = request("GET", "https://api.github.com/repos/minetest/minetest/releases/latest", {
|
||||
headers: {
|
||||
"User-Agent": "https://github.com/GreenXenith/minetestbot/ Minetest latest version fetcher"
|
||||
}
|
||||
});
|
||||
version = JSON.parse(res.getBody('utf8')).tag_name;
|
||||
|
||||
if (!config.color) config.color = "#66601c"; // Configure your bot color or I'll pick an ugly one for you
|
||||
|
||||
module.exports = {
|
||||
color: parseInt(config.color.replace("#", ""), 16),
|
||||
version: version,
|
||||
};
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"prefix": "!",
|
||||
"token": "Bot ABCDEFGHIJKLMNOPQRSTUVWXYZ.1234567890",
|
||||
"color": "#FF0000"
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
const fs = require("fs");
|
||||
const Discord = require("discord.js");
|
||||
const {prefix, token} = require('./config.json');
|
||||
|
||||
// Error if missing configuration
|
||||
if (!token || !prefix) {
|
||||
console.log("Error: Missing configurations! See config.json.example.");
|
||||
return;
|
||||
}
|
||||
|
||||
const client = new Discord.Client();
|
||||
client.commands = new Discord.Collection();
|
||||
|
||||
client.pages = new Discord.Collection();
|
||||
client.pageControls = {
|
||||
prev: "⬅",
|
||||
next: "➡",
|
||||
exit: "🇽"
|
||||
};
|
||||
|
||||
// Find term function
|
||||
client.searchText = function(text, term) {
|
||||
let results = [];
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const l = lines[i];
|
||||
if (l.toLowerCase().includes(term.toLowerCase())) results.push([i + 1, l]);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
// Load commands
|
||||
const commandFiles = fs.readdirSync("./commands").filter(file => file.endsWith(".js"));
|
||||
for (const file of commandFiles) {
|
||||
let command = require(`./commands/${file}`);
|
||||
if (typeof(command) === "function") command = command(client); // Pass client if needed
|
||||
client.commands.set(command.name, command);
|
||||
|
||||
if (command.page) client.pages.set(command.name, command.page);
|
||||
}
|
||||
|
||||
let mentionString = "";
|
||||
|
||||
client.once("ready", () => {
|
||||
console.log(`Logged in as ${client.user.tag}.`);
|
||||
mentionString = `<@${client.user.id}>`
|
||||
|
||||
client.user.setActivity("no one.", {type: "LISTENING"});
|
||||
});
|
||||
|
||||
client.on("message", async message => {
|
||||
// Pingsock >:/
|
||||
if (message.content === mentionString) {
|
||||
const pingsock = client.guilds.get("531580497789190145").emojis.find(emoji => emoji.name === "pingsock");
|
||||
message.channel.send(`${pingsock}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.author.bot) return;
|
||||
|
||||
// Try prefix first, then mentionString (could probably be done better)
|
||||
let p = prefix;
|
||||
if (!message.content.startsWith(p)) {
|
||||
p = `${mentionString} `;
|
||||
if (!message.content.startsWith(p)) return;
|
||||
}
|
||||
|
||||
const args = message.content.slice(p.length).trim().split(/ +/g);
|
||||
const commandName = args.shift().toLowerCase();
|
||||
|
||||
const command = client.commands.get(commandName)
|
||||
|| client.commands.find(cmd => cmd.aliases && cmd.aliases.includes(commandName));
|
||||
|
||||
if (!command) return;
|
||||
|
||||
try {
|
||||
command.execute(message, args, client);
|
||||
} catch(error) {
|
||||
console.error(error);
|
||||
message.channel.send(":warning: Yikes, something broke.");
|
||||
}
|
||||
});
|
||||
|
||||
// Page handler
|
||||
client.on("messageReactionAdd", (reaction, user) => {
|
||||
const message = reaction.message;
|
||||
if (message.author != client.user || user == client.user) return; // Message author must be bot; Reactor must not be bot
|
||||
|
||||
if (!message.embeds.length) return;
|
||||
const embed = message.embeds[0];
|
||||
|
||||
if (!embed.footer) return;
|
||||
if (!(new RegExp(/^Page /).test(embed.footer.text))) return;
|
||||
const matches = embed.footer.text.match(/^Page (\d+) ?\/ ?(\d+) \| (.+)/);
|
||||
const commandName = matches[3];
|
||||
const command = client.pages.get(commandName)
|
||||
|
||||
if (!command || !reaction.me) return;
|
||||
|
||||
let event = ""; // Unused internally; Up to commands to utilize if needed
|
||||
for (const [action, name] of Object.entries(client.pageControls)) {
|
||||
if (reaction.emoji.name === name) {
|
||||
event = action;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (event === "") return;
|
||||
|
||||
let page = parseInt(matches[1]);
|
||||
const total = parseInt(matches[2]);
|
||||
|
||||
switch(event) {
|
||||
case "next":
|
||||
page++;
|
||||
if (page > total) page = 1;
|
||||
break;
|
||||
case "prev":
|
||||
page--;
|
||||
if (page < 1) page = total;
|
||||
break;
|
||||
case "exit":
|
||||
const authorID = embed.footer.iconURL.match(/avatars\/(\d+)/)[1];
|
||||
if (authorID == user.id ||
|
||||
message.guild.member(user).hasPermission("MANAGE_MESSAGES")) message.delete();
|
||||
return;
|
||||
}
|
||||
|
||||
// (message, current page, total pages, reaction event)
|
||||
command.execute(message, page, total, event);
|
||||
reaction.remove(user);
|
||||
});
|
||||
|
||||
// Launch
|
||||
client.login(token);
|
1038
minetestbot.lua
1038
minetestbot.lua
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"name": "minetestbot",
|
||||
"version": "2.0.0",
|
||||
"description": "Discord bot with useful Minetest commands.",
|
||||
"main": "minetestbot.js",
|
||||
"dependencies": {
|
||||
"discord-js": "^11.5.1",
|
||||
"request": "^2.88.1",
|
||||
"sync-request": "^6.1.0"
|
||||
},
|
||||
"author": "GreenXenith",
|
||||
"license": "MIT"
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
const request = require("request");
|
||||
const {color} = require("./config.js");
|
||||
|
||||
module.exports = {
|
||||
searchText: function(text, term) {
|
||||
let results = [];
|
||||
const lines = text.split("\n");
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const l = lines[i];
|
||||
if (l.toLowerCase().includes(term.toLowerCase())) results.push([i + 1, l]);
|
||||
}
|
||||
return results;
|
||||
},
|
||||
pageFooter: function(message, command, page, total) {
|
||||
return {
|
||||
icon_url: message.author.avatarURL,
|
||||
text: `Page ${page} / ${total} | ${command}`
|
||||
};
|
||||
},
|
||||
getPage: function(command, message, config, term, func) {
|
||||
request(`${config.url.search}`, async function (err, res, body) {
|
||||
if (err || res.statusCode != 200) {
|
||||
message.channel.send(":warning: Something went wrong.");
|
||||
return;
|
||||
}
|
||||
|
||||
let embed = {};
|
||||
const fields = [];
|
||||
const results = await module.exports.searchText(body, term);
|
||||
|
||||
if (results.length > 100) {
|
||||
embed = {
|
||||
title: "Error: Result overflow!",
|
||||
description: `Got ${results.length} results. Search [the page](${config.url.display}) manually instead.`,
|
||||
color: color
|
||||
};
|
||||
} else {
|
||||
for (let i = (config.page - 1) * config.pageSize; i < (config.page * config.pageSize); i++) {
|
||||
const res = results[i];
|
||||
if (!res) break;
|
||||
fields.push({
|
||||
name: `Line ${res[0]}:`,
|
||||
value: `[\`\`\`\n${res[1]}\n\`\`\`](${config.url.display}#L${res[0]})`
|
||||
})
|
||||
}
|
||||
|
||||
embed = {
|
||||
title: config.title,
|
||||
thumbnail: {
|
||||
url: config.thumbnail
|
||||
},
|
||||
description: `Results for [\`${term}\`](${config.url.display}):`,
|
||||
color: color,
|
||||
footer: module.exports.pageFooter(message, command, config.page, Math.ceil(results.length / config.pageSize)),
|
||||
fields: fields
|
||||
};
|
||||
}
|
||||
|
||||
func(embed, results);
|
||||
})
|
||||
},
|
||||
addControls: async function(message, turn, exit) {
|
||||
try {
|
||||
const controls = message.client.pageControls;
|
||||
if (turn != false) {
|
||||
await message.react(controls.prev);
|
||||
await message.react(controls.next);
|
||||
}
|
||||
if (exit != false) await message.react(controls.exit);
|
||||
} catch (error) {
|
||||
console.error(`Failed to add page controls. Error: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
326
relay.lua
326
relay.lua
|
@ -1,326 +0,0 @@
|
|||
-- Settings
|
||||
local relay = mbot.relay
|
||||
local default_avatar = "https://i.imgur.com/qa6QJZl.png"
|
||||
|
||||
if not relay.irc_channel or not relay.minetest_user or not relay.discord_channel or not relay.webhook then
|
||||
print("IRC Relay configured incorrectly! Aborting.")
|
||||
client:quit()
|
||||
return
|
||||
end
|
||||
|
||||
-- Mainly for code blocks. Might extend later.
|
||||
local function simplifyMarkdown(input)
|
||||
return input:gsub("\n", " "):gsub(" ", "")
|
||||
end
|
||||
|
||||
-- Get user avatar if they exist
|
||||
local function cloneAvatar(name)
|
||||
local avatar
|
||||
local channel = client:getChannel(relay.discord_channel).guild
|
||||
channel.members:forEach(function(m)
|
||||
if m.user.username == name then
|
||||
avatar = m.user.avatarURL
|
||||
end
|
||||
end)
|
||||
return avatar
|
||||
end
|
||||
|
||||
-- Find @mentions and replace them
|
||||
local function userMention(msg)
|
||||
local channel = client:getChannel(relay.discord_channel).guild
|
||||
channel.members:forEach(function(m)
|
||||
msg = msg:gsub("@"..m.user.username.."#?%d?%d?%d?%d?", m.user.mentionString)
|
||||
msg = msg:gsub("@"..m.user.username:lower().."#?%d?%d?%d?%d?", m.user.mentionString)
|
||||
if m.nickname then
|
||||
msg = msg:gsub("@"..m.nickname.."#?%d?%d?%d?%d?", m.user.mentionString)
|
||||
msg = msg:gsub("@"..m.nickname:lower().."#?%d?%d?%d?%d?", m.user.mentionString)
|
||||
end
|
||||
end)
|
||||
return msg
|
||||
end
|
||||
|
||||
-- Find #channels and replace them
|
||||
local function channelMention(msg)
|
||||
local channel = client:getChannel(relay.discord_channel).guild
|
||||
channel.textChannels:forEach(function(c)
|
||||
msg = msg:gsub("#"..c.name, c.mentionString)
|
||||
end)
|
||||
return msg
|
||||
end
|
||||
|
||||
-- Convert IRC to Discord markdown
|
||||
local function discordFormat(str)
|
||||
-- Get occurences
|
||||
local function find(input, search)
|
||||
local ct = 0
|
||||
for char in string.gmatch(input, ".") do if char == search then ct = ct + 1 end end
|
||||
return ct
|
||||
end
|
||||
|
||||
-- Italic, bold, and underline chars
|
||||
local pat = "["..string.char(0x1D)..string.char(0x02)..string.char(0x1F).."]"
|
||||
|
||||
-- Formats
|
||||
local formatChars = {
|
||||
italic = {string.char(0x1D), "_"},
|
||||
bold = {string.char(0x02), "**"},
|
||||
underline = {string.char(0x1F), "__"},
|
||||
}
|
||||
|
||||
-- Find pairs
|
||||
str = str:gsub(pat.."+.-"..pat.."+", function(section)
|
||||
local wrap = section:sub(section:match("^"..pat.."+"):len()+1,-section:match(pat.."+$"):len()-1)
|
||||
for _, chars in pairs(formatChars) do
|
||||
local found = find(section, chars[1])
|
||||
if found and found > 1 then
|
||||
wrap = chars[2]..wrap..chars[2]
|
||||
end
|
||||
end
|
||||
if wrap == "" then
|
||||
wrap = section
|
||||
end
|
||||
return wrap
|
||||
end)
|
||||
|
||||
-- Find unmatched (should only be 0 or 1)
|
||||
str = str:gsub(pat.."+.-$", function(section)
|
||||
local wrap = section:sub(section:match("^"..pat.."+"):len()+1)
|
||||
for _, chars in pairs(formatChars) do
|
||||
local found = find(section, chars[1])
|
||||
if found and found == 1 then
|
||||
wrap = chars[2]..wrap..chars[2]
|
||||
end
|
||||
end
|
||||
return wrap
|
||||
end)
|
||||
return str
|
||||
end
|
||||
|
||||
-- Combine above
|
||||
local function smartParse(msg)
|
||||
msg = userMention(msg)
|
||||
msg = channelMention(msg)
|
||||
return discordFormat(msg)
|
||||
end
|
||||
|
||||
-- Send webhook message
|
||||
local function hook(payload)
|
||||
-- Content
|
||||
local send = {
|
||||
username = payload.name,
|
||||
avatar_url = payload.avatar,
|
||||
content = payload.msg or "ERROR: Missing message content"
|
||||
}
|
||||
|
||||
-- Send to webhook
|
||||
coroutine.wrap(function()
|
||||
http.request("POST", relay.webhook, {{"Content-Type", "application/json"}}, json.encode(send))
|
||||
end)()
|
||||
end
|
||||
|
||||
-- Create relay user
|
||||
local c = irc:new(relay.server or "irc.freenode.net", "Discord", {auto_join={relay.irc_channel}})
|
||||
|
||||
-- Connect
|
||||
c:connect()
|
||||
|
||||
-- Log/info
|
||||
c:on("connect", function()
|
||||
print("Discord-IRC relay connected.")
|
||||
hook({msg = "Relay connected.", avatar = default_avatar})
|
||||
end)
|
||||
|
||||
c:on("disconnect", function(reason)
|
||||
hook({msg = "Relay disconnected.", avatar = default_avatar})
|
||||
print(string.format("Disconnected: %s", reason))
|
||||
end)
|
||||
|
||||
-- Send IRC chat to Discord
|
||||
c:on("message", function(from, to, msg)
|
||||
local avatar = cloneAvatar(from)
|
||||
if relay.minetest_user and from == relay.minetest_user then
|
||||
avatar = nil
|
||||
end
|
||||
hook({name = from, msg = smartParse(msg), avatar = avatar})
|
||||
end)
|
||||
|
||||
c:on("action", function(from, to, msg)
|
||||
local avatar = cloneAvatar(from)
|
||||
hook({name = from, msg = "*"..smartParse(msg).."*", plain = true, avatar = avatar})
|
||||
end)
|
||||
|
||||
-- Send Discord chat to IRC
|
||||
client:on("messageCreate", function(message)
|
||||
-- Dont send relay messages; stay in desegnated channel
|
||||
if message.member and message.channel.id == relay.discord_channel then
|
||||
-- Get nickname, if any
|
||||
local member = message.member or message.author
|
||||
-- Send commands from Discord to IRC for server to catch
|
||||
if message.content:lower():match("^"..relay.minetest_user:lower()..",") then
|
||||
c:say(relay.irc_channel, "Command sent by "..member.name..":")
|
||||
c:say(relay.irc_channel, simplifyMarkdown(message.content))
|
||||
else
|
||||
c:say(relay.irc_channel, "<"..member.name.."> "..simplifyMarkdown(message.content))
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
--[[c:on("notice", function(from, to, msg)
|
||||
from = from or c.server
|
||||
print(string.format("-%s:%s- %s", from, to, msg))
|
||||
end)]]
|
||||
|
||||
-- IRC actions
|
||||
c:on("ijoin", function(channel, whojoined)
|
||||
print(string.format("Joined channel: %s", channel))
|
||||
|
||||
channel:on("join", function(whojoined)
|
||||
hook({msg = string.format("_%s_ has joined the channel", whojoined), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
channel:on("part", function(who, reason)
|
||||
hook({msg = string.format("_%s_ has left the channel", who)..(reason and " ("..reason..")" or ""), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
channel:on("kick", function(who, by, reason)
|
||||
hook({msg = string.format("_%s_ has been kicked from the channel by %s", who, by)..(reason and " ("..reason..")" or ""), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
channel:on("quit", function(who, reason)
|
||||
hook({msg = string.format("_%s_ has quit", who)..(reason and " ("..reason..")" or ""), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
channel:on("kill", function(who)
|
||||
hook({msg = string.format("_%s_ has been forcibly terminated by the server", who), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
channel:on("+mode", function(mode, setby, param)
|
||||
if setby == nil then return end
|
||||
hook({msg = string.format("_%s_ sets mode: %s%s",
|
||||
channel,
|
||||
setby,
|
||||
"+"..mode.flag,
|
||||
(param and " "..param or "")
|
||||
), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
channel:on("-mode", function(mode, setby, param)
|
||||
if setby == nil then return end
|
||||
hook({msg = string.format("_%s_ sets mode: %s%s",
|
||||
channel,
|
||||
setby,
|
||||
"-"..mode.flag,
|
||||
(param and " "..param or "")
|
||||
), avatar = default_avatar})
|
||||
end)
|
||||
end)
|
||||
|
||||
c:on("ipart", function(channel, reason)
|
||||
print("Left channel")
|
||||
end)
|
||||
|
||||
c:on("ikick", function(channel, kickedby, reason)
|
||||
print(string.format("Kicked from channel by %s (%s)", kickedby, reason))
|
||||
end)
|
||||
|
||||
c:on("names", function(channel)
|
||||
local users = ""
|
||||
for _, user in pairs(channel.users) do
|
||||
users = users .. tostring(user) .. ", "
|
||||
end
|
||||
hook({msg = "Users in channel: "..users:sub(1,-3), avatar = default_avatar})
|
||||
end)
|
||||
|
||||
if relay.dm_enabled == false then
|
||||
return
|
||||
end
|
||||
|
||||
-- Response handler
|
||||
local relayQueue = {}
|
||||
local loggedIn
|
||||
|
||||
-- Relay interaction
|
||||
mbot.register_command("irc", {
|
||||
description = "Interact with IRC relay",
|
||||
func = function(message)
|
||||
-- Only work in DM (to protect passwords when logging in)
|
||||
if message.channel.type ~= 1 then
|
||||
message.channel:send("This can only be used in a private DM!")
|
||||
return
|
||||
end
|
||||
-- Logout current login to prevent abuse if not the same user
|
||||
if loggedIn and loggedIn ~= message.channel.id then
|
||||
-- Handle logout response message
|
||||
relayQueue[#relayQueue+1] = loggedIn
|
||||
c:say(relay.minetest_user, "logout")
|
||||
loggedIn = nil
|
||||
end
|
||||
-- Get message parts
|
||||
local send = message.content
|
||||
local args = send:split(" ", 2)
|
||||
local cmd = args[2]
|
||||
if not cmd then
|
||||
message.channel:send("Empty command!")
|
||||
return
|
||||
end
|
||||
-- Handle server PMs
|
||||
if cmd:match("^@") then
|
||||
-- Add Discord sender
|
||||
args[3] = "<"..message.author.tag.."> "..args[3]
|
||||
end
|
||||
-- Create message
|
||||
send = args[2].." "..args[3]
|
||||
-- Handle response message and send
|
||||
relayQueue[#relayQueue+1] = message.channel.id
|
||||
c:say(relay.minetest_user, send)
|
||||
end,
|
||||
})
|
||||
|
||||
c:on("pm", function (from, msg)
|
||||
-- Is the message a PM from a Minetest user
|
||||
if msg:match("^<[%w_-]-> ") then
|
||||
-- Get message parts
|
||||
local params = msg:split(" ", 2)
|
||||
local from = params[1]:sub(2,-2)
|
||||
local to = params[2]
|
||||
local send = params[3]
|
||||
local dm
|
||||
local channel = client:getChannel(relay.discord_channel).guild
|
||||
-- Find user
|
||||
channel.members:forEach(function(m)
|
||||
if to:match("#%d%d%d%d$") then
|
||||
if m.user.tag == to then
|
||||
dm = m.user
|
||||
end
|
||||
else
|
||||
if m.user.username == to then
|
||||
dm = m.user
|
||||
end
|
||||
end
|
||||
end)
|
||||
-- If the user exists, send the messsage to them
|
||||
if dm then
|
||||
coroutine.wrap(function()
|
||||
dm:getPrivateChannel():send("<"..from.."@"..relay.minetest_user.."> "..smartParse(send))
|
||||
end)()
|
||||
-- Otherwise deny message; use dummy queue to handle PM response
|
||||
else
|
||||
relayQueue[#relayQueue+1] = "dummy"
|
||||
c:say(relay.minetest_user, "@"..from.." User '"..to.."' is not on Discord!")
|
||||
end
|
||||
-- Is this an IRC response
|
||||
else
|
||||
coroutine.wrap(function()
|
||||
-- Send to next channel in queue if not dummy
|
||||
if relayQueue[1] and relayQueue[1] ~= "dummy" then
|
||||
-- Handle user logins
|
||||
if msg:match("^You are now logged in as ") then
|
||||
loggedIn = relayQueue[1]
|
||||
end
|
||||
client:getChannel(relayQueue[1]):send("<"..from.."> "..smartParse(msg))
|
||||
end
|
||||
-- Increment queue
|
||||
table.remove(relayQueue, 1)
|
||||
end)()
|
||||
end
|
||||
end)
|
|
@ -1,27 +0,0 @@
|
|||
-- Bot settings
|
||||
botSettings = {
|
||||
token = "Bot ABCDEFG", -- Bot Token
|
||||
prefix = "~", -- Command Prefix
|
||||
color = "#026610", -- Main embed color (hexidecimal or rgb {r=255,g=255,b=255} table)
|
||||
}
|
||||
|
||||
-- Per-server settings
|
||||
botSettings.servers = {
|
||||
["My Awesome Discord Server"] = {
|
||||
rules = {
|
||||
{"Rule number one", "Subtext"},
|
||||
{"Rule number two", ""},
|
||||
},
|
||||
bot_channel = "<#1234567890>", -- ID for bot command channel (currently unused)
|
||||
},
|
||||
}
|
||||
|
||||
-- Discord-IRC Relay
|
||||
botSettings.relay = {
|
||||
enabled = false,
|
||||
dm_enabled = true, -- Whether or not users can communicate with the server through Discord (default: true)
|
||||
irc_channel = "#my-irc-channel",
|
||||
minetest_user = "MyServerUser", -- Name of user IRC mod uses to connect (found in minetest.conf)
|
||||
discord_channel = "1234567890", -- ID for IRC to relay with
|
||||
webhook = "https://discordapp.com/api/webhooks/1234567890/abcdefghijklmnopqrstuvwxyz1234567890", -- Webhook URL
|
||||
}
|
281
util.lua
281
util.lua
|
@ -1,281 +0,0 @@
|
|||
_G.mbot = {}
|
||||
|
||||
dofile("settings.lua")
|
||||
|
||||
function mbot.dbg(msg)
|
||||
discordia.Logger(4, tostring(os.date())):log(4, tostring(msg))
|
||||
end
|
||||
|
||||
if not botSettings then
|
||||
print("Bot configured incorrectly! Aborting.")
|
||||
client:quit()
|
||||
return
|
||||
end
|
||||
|
||||
for type, settings in pairs(botSettings) do
|
||||
mbot[type] = settings
|
||||
end
|
||||
|
||||
mbot.stable_version = "5.0.1"
|
||||
mbot.unstable_version = "5.1.0-dev"
|
||||
|
||||
mbot.commands = {}
|
||||
mbot.aliases = {}
|
||||
|
||||
-- Get custom emotes
|
||||
function mbot.botEmoji()
|
||||
local list = {}
|
||||
local emoteServer = client:getGuild("531580497789190145")
|
||||
for i in pairs(emoteServer.emojis) do
|
||||
local emoji = emoteServer:getEmoji(i)
|
||||
list[emoji.name] = ":"..emoji.name..":"..i
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Split function
|
||||
function string:split(delimiter, max)
|
||||
local result = {}
|
||||
for match in (self..delimiter):gmatch("(.-)"..delimiter) do
|
||||
if max and #result == max then
|
||||
result[max+1] = self
|
||||
break
|
||||
end
|
||||
table.insert(result, match)
|
||||
self = self:sub(match:len()+2)
|
||||
end
|
||||
return result
|
||||
end
|
||||
|
||||
-- Get RGB int
|
||||
function mbot.getColor(r, g, b)
|
||||
if type(r) == "string" then
|
||||
r = r:gsub("^#", "")
|
||||
if not r:find("^%x%x%x%x%x%x$") then
|
||||
return
|
||||
end
|
||||
r, g, b = tonumber("0x"..r:sub(1,2)), tonumber("0x"..r:sub(3,4)), tonumber("0x"..r:sub(5,6))
|
||||
end
|
||||
return 256 * 256 * r + 256 * g + b
|
||||
end
|
||||
|
||||
-- Get main bot color
|
||||
if type(botSettings.color) == "table" then
|
||||
mbot.color = mbot.getColor(botSettings.color.r, botSettings.color.g, botSettings.color.b)
|
||||
else
|
||||
mbot.color = mbot.getColor(botSettings.color)
|
||||
end
|
||||
|
||||
--[[ URL Handling ]]--
|
||||
local function readUrl(url)
|
||||
local _, body = http.request("GET", url)
|
||||
local lines = {}
|
||||
function adjust(s)
|
||||
if s:sub(-1)~="\n" then s=s.."\n" end
|
||||
return s:gmatch("(.-)\n")
|
||||
end
|
||||
for line in adjust(body) do
|
||||
lines[#lines+1] = line
|
||||
end
|
||||
return lines
|
||||
end
|
||||
|
||||
function mbot.searchUrl(user, url, term, def, id, page)
|
||||
-- Init and defaults
|
||||
local pages = 1
|
||||
local results = {}
|
||||
local resultMax = def.max or 10
|
||||
local resultIcon = def.icon or "https://magentys.io/wp-content/uploads/2017/04/github-logo-1.png" --github logo
|
||||
local resultTitle = def.title or "Search Results"
|
||||
|
||||
if not id or type(id) ~= "string" then
|
||||
return
|
||||
end
|
||||
|
||||
if not page then
|
||||
page = 1
|
||||
end
|
||||
|
||||
-- Adjust URL
|
||||
local cutoff = url:find("%.com/")
|
||||
url = url:sub(cutoff+5):gsub("#%w+", "")
|
||||
|
||||
local githubUrl = "https://github.com/"..url
|
||||
local rawUrl = "https://raw.githubusercontent.com/"..url:gsub("/blob", "", 1)
|
||||
|
||||
-- Read the API
|
||||
for num, line in pairs(readUrl(rawUrl)) do
|
||||
-- Add a field with the line number and a preview (link)
|
||||
if line:lower():find(term:lower(), 1, true) or line:lower():find(term:lower():gsub(" ", "_"), 1, true) then
|
||||
results[#results+1] = {
|
||||
name = "Line "..tostring(num)..":",
|
||||
value = "[```\n"..line:gsub("[%[%]]", "").."\n```]("..githubUrl.."#L"..num..")"
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
local fields = {}
|
||||
|
||||
-- Did we get anything?
|
||||
if #results == 0 then
|
||||
local embed = {
|
||||
title = resultTitle,
|
||||
description = "No results!",
|
||||
color = mbot.color
|
||||
}
|
||||
return embed
|
||||
end
|
||||
|
||||
-- Did we get more than max results?
|
||||
if #results > resultMax then
|
||||
-- Did we get way too many?
|
||||
if #results > 100 then
|
||||
local embed = {
|
||||
title = "Error: Result overflow!",
|
||||
description = "Got "..#results.." results. Search [the URL]("..githubUrl..") manually instead.",
|
||||
color = mbot.color
|
||||
}
|
||||
return embed
|
||||
end
|
||||
pages = math.ceil(#results / resultMax )
|
||||
for i = 1, #results do
|
||||
if i > resultMax*(page-1) and i <= resultMax*(page) then
|
||||
fields[#fields+1] = results[i]
|
||||
end
|
||||
end
|
||||
else
|
||||
fields = table.deepcopy(results)
|
||||
end
|
||||
|
||||
local embed = {
|
||||
title = resultTitle,
|
||||
thumbnail = {
|
||||
url = resultIcon,
|
||||
},
|
||||
description = "Results for [`"..term.."`]("..githubUrl.."):",
|
||||
color = mbot.color,
|
||||
footer = {
|
||||
icon_url = user.avatarURL,
|
||||
text = "Page "..page.."/"..pages.." | "..id
|
||||
},
|
||||
fields = fields,
|
||||
}
|
||||
|
||||
return embed
|
||||
end
|
||||
|
||||
function mbot.pageTurn(reaction, userId)
|
||||
local message = reaction.message
|
||||
local reactor = client:getUser(userId)
|
||||
local embed = message.embed
|
||||
local sender = message.author.name
|
||||
-- Was this a bot message, was it a normal user reacting, and does it have a footer to read?
|
||||
if sender == client.user.name and reactor.name ~= client.user.name and embed and embed.footer then
|
||||
local invoker = mbot.iconUser(embed)
|
||||
if embed.provider then
|
||||
mbot.dbg(embed.provider)
|
||||
end
|
||||
local text = embed.footer.text:gsub(" |", ""):split(" ")
|
||||
-- Is this worth doing something with
|
||||
if text[1] ~= "Page" then
|
||||
return
|
||||
end
|
||||
-- Clean up extras
|
||||
message:removeReaction(reaction, userId)
|
||||
-- Only 3 valid interaction emotes
|
||||
if reaction.emojiName == "⬅" or reaction.emojiName == "➡" or reaction.emojiName == "❌" then
|
||||
-- No ID to work with
|
||||
if not mbot.commands[text[3]] or not mbot.commands[text[3]].page then
|
||||
return
|
||||
end
|
||||
|
||||
-- Remove search
|
||||
if reaction.emojiName == "❌" then
|
||||
if (invoker and reactor.name == invoker.name) or message.guild:getMember(reactor.id):hasPermission("manageMessages") then
|
||||
message:delete()
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
-- Get total and current
|
||||
local page_total = tonumber(text[2]:match("%d+$"))
|
||||
-- This only has 1 page, dont do anything
|
||||
if page_total == 1 then
|
||||
return
|
||||
end
|
||||
local current_page = tonumber(text[2]:match("^%d+"))
|
||||
if reaction.emojiName == "➡" then
|
||||
-- Loop around
|
||||
if current_page == page_total then
|
||||
current_page = 1
|
||||
-- Or go forward
|
||||
else
|
||||
current_page = current_page + 1
|
||||
end
|
||||
else
|
||||
-- Loop around
|
||||
if current_page == 1 then
|
||||
current_page = page_total
|
||||
-- Or go backward
|
||||
else
|
||||
current_page = current_page - 1
|
||||
end
|
||||
end
|
||||
local input, type = mbot.commands[text[3]].page({
|
||||
current = current_page,
|
||||
embed = embed,
|
||||
})
|
||||
-- Edit the message
|
||||
if type == "fields" then
|
||||
message:setEmbed({
|
||||
title = embed.title or nil,
|
||||
thumbnail = embed.thumbnail or nil,
|
||||
description = embed.description or nil,
|
||||
color = mbot.color,
|
||||
footer = {
|
||||
text = "Page "..current_page.."/"..page_total.." | "..text[3]
|
||||
},
|
||||
fields = input,
|
||||
})
|
||||
else
|
||||
message:setEmbed(input)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Other ]]--
|
||||
function mbot.iconUser(embed)
|
||||
if not embed.footer.icon_url then
|
||||
return
|
||||
end
|
||||
return client:getUser(embed.footer.icon_url:match("avatars/%d+"):sub(9))
|
||||
end
|
||||
|
||||
--[[ Command Registration ]]--
|
||||
function mbot.register_command(name, def)
|
||||
-- Not valid
|
||||
if not def.func then
|
||||
return
|
||||
end
|
||||
if def.description and type(def.description) ~= "string" then
|
||||
def.description = nil
|
||||
end
|
||||
if def.aliases and type(def.aliases) ~= "table" then
|
||||
def.aliases = {}
|
||||
end
|
||||
mbot.commands[name] = {
|
||||
func = def.func,
|
||||
description = def.description,
|
||||
usage = def.usage,
|
||||
aliases = def.aliases,
|
||||
page = def.page,
|
||||
secret = def.secret,
|
||||
perms = def.perms,
|
||||
}
|
||||
if def.aliases then
|
||||
for _,alias in pairs(def.aliases) do
|
||||
mbot.aliases[alias] = name
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue