Compare commits

9 Commits

Author SHA1 Message Date
2f71f2b11c Updating ?help for 1.2 release 2025-02-15 13:55:05 -06:00
c71789e949 Added Pirate vs. Ninja commands 2024-11-14 19:07:33 -06:00
384dcc6416 Added ?trace command 2024-11-14 18:48:27 -06:00
d3801b48e0 Added new aliases 2024-11-10 22:19:47 -06:00
978f6bc932 Rewrote ytlookup to fix edge case issues 2024-11-10 13:14:17 -06:00
81523028ae Added ytlookup function 2024-11-09 14:22:29 -06:00
08d023076b Fixed reconnection starting duplicate timers 2024-10-29 18:47:25 -05:00
729d6b41be Added MOTD exclusion system 2024-10-27 16:05:44 -05:00
7162eb7d93 Minor cleanup 2024-10-27 11:23:12 -05:00
5 changed files with 268 additions and 53 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
# ignore all files in export dir
export/
modules/
releases/
# ignore bot settings file
settings.json

View File

@@ -1,6 +1,12 @@
# Changelog
## Release Version 1.2
- Added [Pirate vs. Ninja](https://majcher.com/project/pvn/pvn.cgi) commands: `?pirate`, `?ninja`, `?pvn`, `?nvp`
- Added `?trace` debug tool
- New aliases: `?fineart`, `?artishard`, `?indeed`, `?florida`, `?stop`
- Added YouTube search command
- Fixed reconnection starting duplicate timers
- Added system to prevent MOTD from chosing the same daily result consecutively
- Added support for MOTDs matching only the date
- General purpose functions have been split off into IzzComLib
- Improved fallbacks for file-reading commands

View File

@@ -1,6 +1,7 @@
import hxdiscord.DiscordClient;
import hxdiscord.types.*;
import hxdiscord.endpoints.Endpoints;
import htmlparser.HtmlDocument;
import haxe.Timer;
import haxe.Json;
import sys.io.File;
@@ -23,6 +24,7 @@ class Onequestionmark {
static var motdTimer:Timer;
static var saveTimer:Timer;
static var saveQueue:Array<String> = [];
static var timerInit:Bool = false;
static function main() {
@@ -69,6 +71,11 @@ class Onequestionmark {
Sys.println('[${timestamp()}] Did not find echobox-db.json, generating');
File.saveContent("echobox-db.json", Json.stringify(echoboxDB, "\t"));
}
if (!FileSystem.exists("export")) {
FileSystem.createDirectory("export");
}
saveSystem();
// Start the bot
Bot = new DiscordClient(settings.token, [3276799], settings.debug);
@@ -92,13 +99,16 @@ class Onequestionmark {
botInfo = Endpoints.getCurrentUser();
if (timerInit == false) {
timerInit = true;
startMotd();
saveSystem();
}
}
/**
* The `onMessageCreate()` event provides the bulk of onequestionmark's functionality. This is what triggers when a new Discord message is received, and contains the bot's command processor.
* The `onMessageCreate()` event provides the bulk of onequestionmark's functionality.
* This is what triggers when a new Discord message is received, and contains the bot's command processor.
*/
public static function onMessageCreate(m:Message) {
// DevMode check
@@ -148,6 +158,11 @@ class Onequestionmark {
}
case "motd":
if (m.author.id == settings.botowner) {
if (args.length != 0) {
if (args[0] == "force") {
m.reply({content:'${getMotd(true)}'}, false);
}
} else {
if (!settings.motd.channels.contains(m.channel_id)) {
settings.motd.channels.push(m.channel_id);
m.reply({content:'MOTD has been enabled for <#${m.channel_id}>'}, false);
@@ -157,6 +172,16 @@ class Onequestionmark {
}
saveQueue.push("settings");
}
}
case "trace":
if (m.author.id == settings.botowner) {
if (m.referenced_message == null) {
Sys.println('[${timestamp()}] trace: ${Json.stringify(getMessage(m.channel_id, m.id))}');
} else {
Sys.println('[${timestamp()}] trace: ${Json.stringify(getMessage(m.referenced_message.channel_id, m.referenced_message.id))}');
}
m.react('');
}
// System for WIP commands that only work in testing server
case "wipcommand":
if (m.guild_id == settings.devserver) {
@@ -179,17 +204,22 @@ class Onequestionmark {
"fields": [
{
"name": "**Bot commands:**",
"value": "`?help`: Posts this help document. :book:\n`?chk`: Replies with \"ack\". :speaking_head:\n`?slap <target>`: The classic mIRC troutslap. :fish:\n`?hug <target (optional)>`: Posts randomized hug image. :people_hugging:\n`?yes`: Posts randomized \"yes\" image or video. :thumbsup:\n`?no`: Posts randomized \"no\" image or video. :no_entry_sign:\n`?mrkrabs`: Similar to `?chk`. :crab:\n`?tufac`: The official Tufac Theme Song. :busts_in_silhouette:\n`?coin <count (optional)>`: Flips between 1 and 100 coins. :coin:\n`?dice.<arg> <num>`: Rolls <arg>-sided die <num> times :game_die:\n`?8ball`: Answers your yes or no questions. :8ball:\n`?echobox`: Posts randomized quote from the quote database. :loudspeaker:\n`?echobox <quote>`: Adds provided quote to the database. :writing_hand:",
"value": "`?help`: Posts this help document. :book:\n`?chk`: Replies with \"ack\". :speaking_head:\n`?slap <target>`: The classic mIRC troutslap. :fish:\n`?hug <target (optional)>`: Posts randomized hug image. :people_hugging:\n`?yes`: Posts randomized \"yes\" image or video. :thumbsup:\n`?no`: Posts randomized \"no\" image or video. :no_entry_sign:\n`?mrkrabs`: Similar to `?chk`. :crab:\n`?tufac`: The official Tufac Theme Song. :busts_in_silhouette:\n`?coin <count (optional)>`: Flips between 1 and 100 coins. :coin:\n`?dice.<arg> <num>`: Rolls <arg>-sided die <num> times :game_die:\n`?8ball`: Answers your yes or no questions. :8ball:\n`?echobox`: Posts randomized quote from the quote database. :loudspeaker:\n`?echobox <quote>`: Adds provided quote to the database. :writing_hand:\n`?youtube <query>`: Return first result of a YouTube search. :cinema:",
"inline": false
},
{
"name": "**Pirate vs. Ninja:**",
"value": "`?pirate <name>`: Look up stats for a pirate. :pirate_flag:\n`?ninja <name>`: Look up stats for a ninja. :ninja:\n`?pvn <pirate> | <ninja>`: Let them fight! :crossed_swords:\n`?nvp <ninja> | <pirate>`: Same as above but reversed order.",
"inline": false
},
{
"name": "**Basic Response Aliases:**",
"value": "`?angery`: *I taste a vegetal.* :rage:\n`?subway`: Arin's infamous Subway rant. :sandwich:\n`?illuminati`: Always watching. :eye:\n`?cube`: *The Cube*\n`?coolsville`: Population: Us :sunglasses:\n`?communism`: The old 3D rotating gif.\n`?opinions`: Here they come. :scream:",
"value": "`?angery`: *I taste a vegetal.* :rage:\n`?subway`: Arin's infamous Subway rant. :sandwich:\n`?illuminati`: Always watching. :eye:\n`?cube`: *The Cube*\n`?coolsville`: Population: Us :sunglasses:\n`?communism`: The old 3D rotating gif.\n`?opinions`: Here they come. :scream:\n`?fineart`: The great masterpiece. :art:\n`?artishard`: Art *is* hard. :corn:\n`?florida`: :carpentry_saw::rabbit:\n`?stop`: :stop_sign: :smiley:\n`?indeed`",
"inline": false
},
{
"name": "**Auth-Requiring Commands:**",
"value": "`?motd`: Enables MOTD for current channel\n`?devmode`: Toggles Dev Mode\n`?quit`: Shutdown command",
"value": "`?motd`: Enables MOTD for current channel.\n`?devmode`: Toggles Dev Mode.\n`?trace`: Debugging tool.\n`?quit`: Shutdown command.",
"inline": true
},
{
@@ -208,6 +238,26 @@ class Onequestionmark {
if (msg.length != 0) {m.reply({content:'*onequestionmark slaps ${msg} around a bit with a large trout*'}, false);}
case "tufac":
m.reply({content:"*Not Teh Face, but better,*\n*Tufac to the letter!*\n*Always two faces, never one,*\n*Tufac has you on the run!*"}, false);
case "youtube":
if (msg.length > 0) {ytlookup(m,msg);}
case "ytdebug":
if (FileSystem.exists("export/ytlookup_result.txt")) {
var debugRename = 'export/${datestamp()}_${StringTools.lpad(Std.string(Date.now().getHours()), "0", 2)}-${StringTools.lpad(Std.string(Date.now().getMinutes()), "0", 2)}-${StringTools.lpad(Std.string(Date.now().getSeconds()), "0", 2)}_ytdebug.txt';
FileSystem.rename("export/ytlookup_result.txt", debugRename);
Sys.println('[${timestamp()}] ytdebug: Created debug file ${debugRename}');
m.react('');
} else {
Sys.println('[${timestamp()}] ytdebug: No result to debug');
m.reply({content:"ytdebug: No result to debug"}, false);
}
case "pirate":
if (msg.length > 0) {pvn("p",msg,m);}
case "ninja":
if (msg.length > 0) {pvn("n",msg,m);}
case "pvn":
if (msg.length > 0) {pvn("pvn",msg,m);}
case "nvp":
if (msg.length > 0) {pvn("nvp",msg,m);}
// Gaming functions
case "coin":
if ((msg.length == 0) || (Std.parseInt(msg) == null) || (Std.parseInt(msg) == 1)) {
@@ -283,6 +333,16 @@ class Onequestionmark {
m.reply({content:"https://cdn.discordapp.com/attachments/1071547517847732305/1147590229960691742/communism.gif"}, false);
case "opinions":
m.reply({content:"https://cdn.discordapp.com/attachments/1071547517847732305/1147983375701921892/opinions.jpg"}, false);
case "fineart":
m.reply({content:"https://cdn.discordapp.com/attachments/270113422232911883/423323629116325898/unknown.png"}, false);
case "artishard":
m.reply({content:"https://cdn.discordapp.com/attachments/270113422232911883/356638565834424330/tumblr_nrxhzoF0KM1t75ioqo2_250.jpg"}, false);
case "indeed":
m.reply({content:"https://cdn.discordapp.com/attachments/1071547517847732305/1305252892491649145/indeed.gif"}, false);
case "florida":
m.reply({content:"https://cdn.discordapp.com/attachments/1071547517847732305/1305253566348529696/florida.gif"}, false);
case "stop":
m.reply({content:"https://cdn.discordapp.com/attachments/1071547517847732305/1305253985036275712/stop.jpg"}, false);
// Image database commands
case "hug":
if (hugDB.length > 0) {
@@ -345,7 +405,8 @@ class Onequestionmark {
/**
* This function starts the "Meme of the Day" system. A temporary timer is set that counts down to the next MOTD event, at which point the main timer is started on a 24 hour loop.
* This function starts the "Meme of the Day" system.
* A temporary timer is set that counts down to the next MOTD event, at which point the main timer is started on a 24 hour loop.
*/
public static function startMotd() {
var now = Date.now();
@@ -353,8 +414,8 @@ class Onequestionmark {
if (now.getHours() >= settings.motd.time) {
if (settings.motd.date != datestamp()) {
settings.motd.date = datestamp();
saveQueue.push("settings");
postMotd();
saveQueue.push("settings");
}
nextPost = DateTools.delta(nextPost, 86400000); // Add one day in milliseconds
}
@@ -364,25 +425,26 @@ class Onequestionmark {
Timer.delay(function(){
settings.motd.date = datestamp();
saveQueue.push("settings");
postMotd();
saveQueue.push("settings");
// Start the looped MOTD timer
motdTimer = new Timer(86400000);
motdTimer.run = function() {
settings.motd.date = datestamp();
saveQueue.push("settings");
postMotd();
saveQueue.push("settings");
}
}, delay);
}
/**
* Sends the MOTD to the channels specified in the bot settings. May just move this code into the places it's needed instead of keeping this function.
* Sends the MOTD to the channels specified in the bot settings.
* May just move this code into the places it's needed instead of keeping this function.
*/
public static function postMotd() {
var msg = getMotd();
var msg = getMotd(true);
var channels:Array<String> = settings.motd.channels; // Because it won't let me do this directly
if (channels.length > 0) {for (i in channels) {if (msg.length > 0) {Endpoints.sendMessage(i, {content:msg}, null, false);}}}
@@ -391,10 +453,13 @@ class Onequestionmark {
/**
* This function checks the MOTD database to find the entry most relevant to the current date and returns the appropriate data.
*
* @param update Whether the result should be stored to exclude from the next run.
*/
public static function getMotd() {
public static function getMotd(update:Bool = false) {
var msg = "";
var day:Array<String> = [];
var special = true;
// Check for exact date
if (motdDB.exists('${datestamp()}')) {
@@ -415,9 +480,150 @@ class Onequestionmark {
// Otherwise, post daily meme
else if (motdDB.exists(printDay().substr(0,3))) {
day = motdDB.get(printDay().substr(0,3));
special = false;
}
if (day.length > 0) {msg = day[randInt(0, day.length-1)];}
if (day.length > 0) {
if (update && !special && (day.length > 2)) {
var today = Date.now().getDay();
var previous:Array<Int> = settings.motd.previous;
var choice = randIntExclude(0, day.length-1,[previous[today]]);
previous[today] = choice;
settings.motd.previous = previous;
msg = day[choice];
} else {
msg = day[randInt(0, day.length-1)];
}
}
return msg;
}
/**
* This function scrapes PVN and replies with the appropriate results.
* @param choice `p`, `n`, `pvn`, or `nvp`.
* @param query The string to search.
*/
public static function pvn(choice:String,query:String,m:Message) {
if (choice.length > 1) {
if (query.contains(" | ")) {
var splitQuery = query.split(" | ");
query = '${splitQuery[0].urlEncode()}&n2=${splitQuery[1].urlEncode()}';
} else {
m.reply({content:'$choice: Must choose two combatants.'}, false);
return;
}
} else {
query = query.urlEncode();
}
var req = new haxe.Http('https://majcher.com/project/pvn/pvn.cgi?a=${choice.urlEncode()}&n1=${query}');
req.onData = function (request) {
var data = new HtmlDocument(request);
var search = data.find("table>tr>td");
try {
m.reply({content:'## ${search[0].innerText}\n${search[2].innerText} ${search[3].innerText}\n${search[4].innerText} ${search[5].innerText}\n${search[6].innerText} ${search[7].innerText}\n${search[8].innerText} ${search[9].innerText}\n**${search[10].innerText} ${search[11].innerText}**'}, false);
if (choice.length > 1) {
var result = data.find('div#result');
m.reply({content:'## ${search[12].innerText}\n${search[14].innerText} ${search[15].innerText}\n${search[16].innerText} ${search[17].innerText}\n${search[18].innerText} ${search[19].innerText}\n${search[20].innerText} ${search[21].innerText}\n**${search[22].innerText} ${search[23].innerText}**'}, false);
m.reply({content:'## ${result[0].innerText}'}, false);
}
} catch(e) {
m.reply({content:'$choice: Error - $e'}, false);
Sys.println('[${timestamp()}] $choice: Error - $e');
}
}
req.onError = function (error) {
m.reply({content:'Error in PVN: $error'}, false);
Sys.println('[${timestamp()}] $choice: Error - $error, request was $query');
}
req.request();
}
/**
* This function performs a YouTube search and replies with the first result.
* @param m The message data.
* @param query The string to search.
*/
public static function ytlookup(m:Message,query:String) {
var req = new haxe.Http('https://www.youtube.com/results?search_query=${query.urlEncode()}');
Sys.println('[${timestamp()}] ytlookup: URL - https://www.youtube.com/results?search_query=${query.urlEncode()}');
req.onData = function (request) {
File.saveContent("export/ytlookup_result.txt", request);
var data = new HtmlDocument(request);
var search = data.find("script"); // YouTube obfuscates everything into JS garbage so we have to check every <script>
var gotcha = "";
var result = "Error: Unable to parse YouTube search result.";
for (i in search) {
if (i.toString().contains("\"videoRenderer\":{\"videoId\":\"") || i.toString().contains("\"reelWatchEndpoint\":{\"videoId\":\"")) {
gotcha = i.toString();
if ((i.toString().contains("\"reelWatchEndpoint\":{\"videoId\":\"")) && (gotcha.indexOf("\"reelWatchEndpoint\":{\"videoId\":\"") < gotcha.indexOf("\"videoRenderer\":{\"videoId\":\""))) {
// Top result is a Short
result = "https://youtu.be/" + gotcha.substring(gotcha.indexOf("\"reelWatchEndpoint\":{\"videoId\":\"")+32, gotcha.indexOf("\"", gotcha.indexOf("\"reelWatchEndpoint\":{\"videoId\":\"")+32));
Sys.println('[${timestamp()}] ytlookup: Result - $result (Short)');
break;
} else {
// Top result is a normal video
result = "https://youtu.be/" + gotcha.substring(gotcha.indexOf("\"videoRenderer\":{\"videoId\":\"")+28, gotcha.indexOf("\"", gotcha.indexOf("\"videoRenderer\":{\"videoId\":\"")+28));
Sys.println('[${timestamp()}] ytlookup: Result - $result (Normal)');
break;
}
}
}
m.reply({content:result}, false);
m.reply({content:"-# YouTube Lookup is still in beta. If the result seems inaccurate, please use the command `?ytdebug` so the developer can review the data that was received."}, false);
}
req.onError = function (error) {
m.reply({content:'Error in YouTube Lookup: $error'}, false);
Sys.println('[${timestamp()}] ytlookup: Error - $error, request was $query');
}
req.request();
}
/**
* Retrieves a specific message in the channel and returns it as a JSON object.
* @param channel_id Numerical ID of the channel.
* @param m_id Numerical ID of the message.
*/
public static function getMessage(channel_id:String, m_id:String) {
var req:hxdiscord.utils.Http = new hxdiscord.utils.Http("https://discord.com/api/v"+hxdiscord.gateway.Gateway.API_VERSION+"/channels/"+channel_id+"/messages/"+m_id);
req.addHeader("User-Agent", "hxdiscord (https://github.com/FurretDev/hxdiscord)");
req.addHeader("Authorization", DiscordClient.authHeader);
req.setMethod("GET");
var msg:Dynamic = null;
req.onData = function(data:String) {
msg = haxe.Json.parse(data);
}
req.onError = function(error) {
Sys.println('[${timestamp()}] getMessage: Error - $error');
Sys.println('[${timestamp()}] getMessage: ${req.responseData}');
}
req.send();
return msg;
}

View File

@@ -14,7 +14,8 @@ A `settings.json` file is required to run the bot. An example is provided below:
"motd": {
"date": "",
"time": 8,
"channels": []
"channels": [],
"previous": [0,0,0,0,0,0,0]
},
"debug": false,
"devmode": false,
@@ -25,7 +26,7 @@ A `settings.json` file is required to run the bot. An example is provided below:
}
```
- onequestionmark will handle most of the `motd` values itself.
- `date` is modified by the bot during normal operations.
- `date` and `previous` are modified by the bot during normal operations.
- `time` determines the hour at which the MOTD is posted (24-hour notation, local time).
- Using the `?motd` command adds the current channel to the `channels` array.
- `debug` determines whether HxDiscord's console debug messages are enabled.
@@ -59,4 +60,4 @@ onequestionmark is designed and tested to run in [NekoVM](https://nekovm.org/dow
`onequestionmark.n` should be placed in the same directory as its JSON files.
It is preferable to stop the bot via its own `?stop` command in Discord, so it can save its settings file before exiting.
It is preferable to stop the bot via its own `?quit` command in Discord, so it can save its settings file before exiting.

View File

@@ -1,6 +1,7 @@
-m Onequestionmark
-L hxdiscord
-L IzzComLib
-L HtmlParser
# hxdiscord uses deprecated functions
-D no-deprecation-warnings
--neko export/onequestionmark.n