Compare commits

7 Commits

Author SHA1 Message Date
0c95d19ad2 Disabled ice response due to fascism 2025-06-24 16:05:08 -05:00
c43d648279 Native PvN implementation 2025-04-01 23:01:39 -05:00
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
3 changed files with 276 additions and 46 deletions

View File

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

View File

@@ -6,6 +6,7 @@ import haxe.Timer;
import haxe.Json;
import sys.io.File;
import sys.FileSystem;
import BigInt;
import izzcomlib.IzzComLib.*;
using StringTools;
@@ -14,7 +15,7 @@ class Onequestionmark {
static var Bot:DiscordClient;
static var settings:Dynamic;
static var botInfo:Dynamic;
static var iceRegex:EReg = ~/\bicc?ed?\b/i;
//static var iceRegex:EReg = ~/\bicc?ed?\b/i;
static var hugDB:Array<String>;
static var motdDB:haxe.DynamicAccess<Dynamic> = {};
@@ -71,6 +72,9 @@ 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();
@@ -170,6 +174,15 @@ 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) {
@@ -192,22 +205,27 @@ 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
},
{
"name": "**Non-command bot functions:**",
"value": "**MOTD**: *Meme of the Day* is posted in enabled channels.\n**Icce**: Bot provides users with ice cuboids. :ice_cube:",
"value": "**MOTD**: *Meme of the Day* is posted in enabled channels.",
"inline": true
}
]
@@ -223,6 +241,24 @@ class Onequestionmark {
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)) {
@@ -298,6 +334,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) {
@@ -353,7 +399,7 @@ class Onequestionmark {
// Non-command responses
if ((msg.charAt(0) == ".") && (msg.length == 1)) {m.reply({content:"omg a meteor"}, true);}
if ((iceRegex.match(msg)) && (m.author.id != botInfo.id)) {m.reply({content:"Did some carbon-based lifeform just say **I C E**?"});}
//if ((iceRegex.match(msg)) && (m.author.id != botInfo.id)) {m.reply({content:"Did some carbon-based lifeform just say **I C E**?"});}
if ((m.mention_everyone == true) && (msg.charAt(0) != "?")) {m.reply({content:"https://cdn.discordapp.com/attachments/1071547517847732305/1147598637241741343/at_everyone.jpg"}, true);}
}
}
@@ -458,15 +504,168 @@ class Onequestionmark {
}
/**
* This function performs the *Pirate vs Ninja* stat calculations and replies with the appropriate results.
* @param choice `p`, `n`, `pvn`, or `nvp`.
* @param query The name of the character. For dual-character choices, separate names with a pipe.
*/
public static function pvn(choice:String,query:String,m:Message) {
if (choice.length > 1) {
var splitQuery:Array<String> = [];
if (query.contains(" | ")) {
splitQuery = query.split(" | ");
} else {
splitQuery.push(query);
splitQuery.push(query);
}
if (choice == "pvn") {
var pirate = getPirate(splitQuery[0]);
var ninja = getNinja(splitQuery[1]);
printPirate(pirate,m);
printNinja(ninja,m);
if (pirate.total > ninja.total) {
m.reply({content:'## ${pirate.rank} ${pirate.name} wins!'}, false);
} else if (pirate.total < ninja.total) {
m.reply({content:'## ${ninja.rank} ${ninja.name} wins!'}, false);
} else {
m.reply({content:'## It\'s a TIE!'}, false);
}
} else if (choice == "nvp") {
var ninja = getNinja(splitQuery[0]);
var pirate = getPirate(splitQuery[1]);
printNinja(ninja,m);
printPirate(pirate,m);
if (pirate.total > ninja.total) {
m.reply({content:'## ${pirate.rank} ${pirate.name} wins!'}, false);
} else if (pirate.total < ninja.total) {
m.reply({content:'## ${ninja.rank} ${ninja.name} wins!'}, false);
} else {
m.reply({content:'## It\'s a TIE!'}, false);
}
}
} else {
if (choice == "p") {printPirate(getPirate(query),m);}
if (choice == "n") {printNinja(getNinja(query),m);}
}
}
public static function getPirate(name) {
var pirate:Dynamic = {};
pirate.name = name;
var pweapons = ['Cutlass', 'Pistol', 'Broken Bottle', 'Cannon', 'Attack Parrot', 'Hook Hand', 'Belaying Pin', 'Dagger', 'Grappling Hook', 'A Mean Streak A Mile Wide'];
pirate.swash = (hashString(name, "swashbuckling", 20) + 1);
pirate.drunk = (hashString(name, 'drunkenness', 20) + 1);
pirate.booty = (hashString(name, 'booty', 20) + 1);
var weapons:Array<String> = [];
var wnum = (hashString(name, "pweapons", 5) - 1);
while (wnum > 0) {
var wsel = pweapons[hashString(name, '$wnum pweapon', pweapons.length - 1)];
if (!weapons.contains(wsel)) {weapons.push(wsel);}
wnum--;
}
pirate.weapons = "";
for (i in 0...weapons.length) {
if (Std.string(pirate.weapons).length != 0) {pirate.weapons += ", ";}
pirate.weapons = pirate.weapons + weapons[i];
}
pirate.total = pirate.swash + pirate.drunk + pirate.booty + weapons.length;
pirate.rank = "Cabin Boy";
if (pirate.total > 15) {pirate.rank = 'Pirate';}
if (pirate.total > 30) {pirate.rank = 'Dread Pirate';}
if (pirate.total > 45) {pirate.rank = 'Captain';}
if (pirate.total >= 60) {pirate.rank = 'Pirate King';}
return(pirate);
}
public static function getNinja(name) {
var ninja:Dynamic = {};
ninja.name = name;
var nweapons = ['Shuriken', 'Katana', 'Poison Dart', 'Death Touch', 'Ninja-To', 'Smoke Bomb', 'Thousand Blossom Finger', 'A Pointy Stick', 'Jo Stick', 'Nunchaku'];
ninja.sneak = (hashString(name, "sneakiness", 20) + 1);
ninja.peejs = (hashString(name, 'pajamas', 20) + 1);
ninja.point = (hashString(name, 'pointy things', 20) + 1);
var weapons:Array<String> = [];
var wnum = (hashString(name, "nweapons", 5) - 1);
while (wnum > 0) {
var wsel = nweapons[hashString(name, '$wnum nweapon', nweapons.length - 1)];
if (!weapons.contains(wsel)) {weapons.push(wsel);}
wnum--;
}
ninja.weapons = "";
for (i in 0...weapons.length) {
if (Std.string(ninja.weapons).length != 0) {ninja.weapons += ", ";}
ninja.weapons = ninja.weapons + weapons[i];
}
ninja.total = ninja.sneak + ninja.peejs + ninja.point + weapons.length;
ninja.rank = "Apprentice";
if (ninja.total > 15) {ninja.rank = 'Ninja';}
if (ninja.total > 30) {ninja.rank = 'Deadly Ninja';}
if (ninja.total > 45) {ninja.rank = 'Ninja Master';}
if (ninja.total >= 60) {ninja.rank = 'Grandmaster';}
return(ninja);
}
public static function printPirate(pirate, m:Message) {
m.reply({content:'## ${pirate.rank} ${pirate.name}\nSwashbuckling: ${pirate.swash}\nDrunkenness: ${pirate.drunk}\nBooty: ${pirate.booty}\nWeapons: ${pirate.weapons}\n**Total: ${pirate.total}**'}, false);
}
public static function printNinja(ninja, m:Message) {
m.reply({content:'## ${ninja.rank} ${ninja.name}\nSneakiness: ${ninja.sneak}\nPajamas: ${ninja.peejs}\nPointy Things: ${ninja.point}\nWeapons: ${ninja.weapons}\n**Total: ${ninja.total}**'}, false);
}
/**
* Marc Majcher's string hasher from *Pirate vs Ninja!*
* @param input The input string.
* @param seed An identifying salt to make strings come out differently for different, but same-sized lists.
* @param num The number of buckets you wish to hash into.
*/
public static function hashString(input:String, seed:String, num:BigInt) {
var count:BigInt = 0;
var total:BigInt = 0;
var xorResult = "";
// Pad strings until they're equal in length
input = StringTools.rpad(input, String.fromCharCode(0), seed.length);
seed = StringTools.rpad(seed, String.fromCharCode(0), input.length);
// XOR the input and seed
for (i in 0...(input.length)) {
xorResult = xorResult + String.fromCharCode(input.charCodeAt(i) ^ seed.charCodeAt(i));
}
// Calculate stats
for (i in 0...xorResult.length) {
total += (xorResult.charCodeAt(i):BigInt) * ((128:BigInt).pow(count));
count++;
}
return(((total * (total + (523:BigInt))) % num).toInt());
}
/**
* 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 http = new haxe.Http('https://www.youtube.com/results?search_query=${query}');
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()}');
http.onData = function (request) {
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>
@@ -474,36 +673,63 @@ class Onequestionmark {
var result = "Error: Unable to parse YouTube search result.";
for (i in search) {
if (i.toString().contains("https://youtu.be/")) { // Basic results link to the video like this
gotcha = i.toString();
result = gotcha.substring(gotcha.indexOf("https://youtu.be/"), gotcha.indexOf(".", gotcha.indexOf("https://youtu.be/")+17));
break;
}
else if (i.toString().contains("watch?v=")) { // Because YouTube isn't consistent, alternative scrape
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));
// Detect YouTube Shorts link and clean it up
if (gotcha.substring(gotcha.indexOf("watch?v=")+8, gotcha.indexOf("\"", gotcha.indexOf("watch?v="))).contains("\\")) {
result = "https://youtu.be/" + gotcha.substring(gotcha.indexOf("watch?v=")+8, gotcha.indexOf("\\", gotcha.indexOf("watch?v=")));
}
// Normal video
else {
result = "https://youtu.be/" + gotcha.substring(gotcha.indexOf("watch?v=")+8, gotcha.indexOf("\"", gotcha.indexOf("watch?v=")));
}
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);
}
http.onError = function (error) {
Sys.println('[${timestamp()}] YTLOOKUP: Error - $error, request was $query');
req.onError = function (error) {
m.reply({content:'Error in YouTube Lookup: $error'}, false);
Sys.println('[${timestamp()}] ytlookup: Error - $error, request was $query');
}
http.request();
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

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