mirror of
https://github.com/osmarks/autobotrobot
synced 2025-03-11 10:58:10 +00:00
Python rewrite
This commit is contained in:
parent
10c27c216f
commit
55f8334326
5
.gitignore
vendored
Normal file → Executable file
5
.gitignore
vendored
Normal file → Executable file
@ -1,2 +1,3 @@
|
||||
.env
|
||||
target
|
||||
config.toml
|
||||
bot-data.json
|
||||
__pycache__
|
2036
Cargo.lock
generated
2036
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@ -1,15 +0,0 @@
|
||||
[package]
|
||||
name = "autobotrobot"
|
||||
version = "0.1.0"
|
||||
|
||||
[dependencies]
|
||||
serenity = "0.5"
|
||||
dotenv = "0.13"
|
||||
calculate = "0.5"
|
||||
ddg = "0.5"
|
||||
regex = "1.0"
|
||||
reqwest = "0.9"
|
||||
lazy_static = "1.2"
|
||||
|
||||
[patch.crates-io]
|
||||
openssl = { git = "https://github.com/ishitatsuyuki/rust-openssl", branch = "0.9.x" }
|
135
src/main.py
Normal file
135
src/main.py
Normal file
@ -0,0 +1,135 @@
|
||||
import discord
|
||||
import toml
|
||||
import logging
|
||||
import subprocess
|
||||
import discord.ext.commands as commands
|
||||
import re
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
import tio
|
||||
|
||||
data = {}
|
||||
data_file = "bot-data.json"
|
||||
|
||||
def save_data():
|
||||
try:
|
||||
with open(data_file, "w") as f:
|
||||
global data
|
||||
json.dump(data, f)
|
||||
except Exception as e:
|
||||
logging.error(f"Error saving data file: {repr(e)}.")
|
||||
|
||||
def load_data():
|
||||
try:
|
||||
with open(data_file, "r") as f:
|
||||
global data
|
||||
data = json.load(f)
|
||||
except Exception as e:
|
||||
logging.warning(f"Error loading data file: {repr(e)}. This is not a critical error.")
|
||||
|
||||
load_data()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(asctime)s %(message)s", datefmt="%H:%M:%S %d/%m/%Y")
|
||||
|
||||
config = toml.load(open("config.toml", "r"))
|
||||
|
||||
bot = commands.Bot(command_prefix='++', description="AutoBotRobot, the most useless bot in the known universe.", case_insensitive=True)
|
||||
bot._skip_check = lambda x, y: False
|
||||
|
||||
cleaner = discord.ext.commands.clean_content()
|
||||
def clean(ctx, text):
|
||||
return cleaner.convert(ctx, text)
|
||||
|
||||
@bot.event
|
||||
async def on_ready():
|
||||
await bot.change_presence(status=discord.Status.online, activity=discord.Activity(type=discord.ActivityType.listening, name="commands beginning with ++"))
|
||||
|
||||
@bot.event
|
||||
async def on_message(message):
|
||||
print(message.content)
|
||||
await bot.process_commands(message)
|
||||
|
||||
@bot.command(help="Gives you a random fortune as generated by `fortune`.")
|
||||
async def fortune(ctx):
|
||||
await ctx.send(subprocess.run(["fortune"], stdout=subprocess.PIPE, encoding="UTF-8").stdout)
|
||||
|
||||
@bot.command(help="Says Pong.")
|
||||
async def ping(ctx):
|
||||
await ctx.send("Pong.")
|
||||
|
||||
@bot.command(help="Deletes the specified target.", rest_is_raw=True)
|
||||
async def delete(ctx, *, raw_target):
|
||||
target = await clean(ctx, raw_target.strip())
|
||||
async with ctx.typing():
|
||||
await ctx.send(f"Deleting {target}...")
|
||||
await asyncio.sleep(1)
|
||||
deleted = data.get("deleted", [])
|
||||
data["deleted"] = deleted + [target]
|
||||
save_data()
|
||||
try: __builtins__.__delattr__(target)
|
||||
except: pass
|
||||
try: del globals()[target]
|
||||
except: pass
|
||||
try: del locals()[target]
|
||||
except: pass
|
||||
await ctx.send(f"Deleted {target} successfully.")
|
||||
|
||||
@bot.command(help="View recently deleted things, optionally matching a filter.")
|
||||
async def list_deleted(ctx, search=None):
|
||||
acc = "Recently deleted:\n"
|
||||
for thing in reversed(data.get("deleted", [])):
|
||||
to_add = "- " + thing + "\n"
|
||||
if len(acc + to_add) > 2000:
|
||||
break
|
||||
if search == None or search in thing: acc += to_add
|
||||
await ctx.send(acc)
|
||||
|
||||
EXEC_REGEX = "^(.*)\n```([a-zA-Z0-9_\\-+]+)\n(.*)```$"
|
||||
|
||||
def make_embed(*, fields=[], footer_text=None, **kwargs):
|
||||
embed = discord.Embed(**kwargs)
|
||||
for field in fields:
|
||||
if len(field) > 2:
|
||||
embed.add_field(name=field[0], value=field[1], inline=field[2])
|
||||
else:
|
||||
embed.add_field(name=field[0], value=field[1], inline=False)
|
||||
if footer_text:
|
||||
embed.set_footer(text=footer_text)
|
||||
return embed
|
||||
|
||||
def error_embed(msg, title="Error"):
|
||||
return make_embed(color=config["colors"]["error"], description=msg, title=title)
|
||||
|
||||
@bot.command(rest_is_raw=True, help="Execute provided code (in a codeblock) using TIO.run.")
|
||||
async def exec(ctx, *, arg):
|
||||
match = re.match(EXEC_REGEX, arg, flags=re.DOTALL)
|
||||
if match == None:
|
||||
await ctx.send(embed=error_embed("Invalid format. Expected a codeblock with language."))
|
||||
return
|
||||
flags = match.group(1)
|
||||
lang = match.group(2)
|
||||
code = match.group(3)
|
||||
|
||||
async with ctx.typing():
|
||||
ok, result, debug = await tio.run(lang, code)
|
||||
if not ok:
|
||||
await ctx.send(embed=error_embed(result, "Execution error"))
|
||||
else:
|
||||
out = result
|
||||
if "debug" in flags: out += debug
|
||||
out = out[:2000]
|
||||
await ctx.send(out)
|
||||
|
||||
@bot.command(help="List supported languages, optionally matching a filter.")
|
||||
async def supported_langs(ctx, search=None):
|
||||
langs = sorted(tio.languages())
|
||||
acc = ""
|
||||
for lang in langs:
|
||||
if len(acc + lang) > 2000:
|
||||
await ctx.send(acc)
|
||||
acc = ""
|
||||
if search == None or search in lang: acc += lang + " "
|
||||
await ctx.send(acc)
|
||||
|
||||
bot.run(config["token"])
|
250
src/main.rs
250
src/main.rs
@ -1,250 +0,0 @@
|
||||
#[macro_use] extern crate serenity;
|
||||
extern crate dotenv;
|
||||
extern crate calc;
|
||||
extern crate ddg;
|
||||
extern crate regex;
|
||||
#[macro_use] extern crate lazy_static;
|
||||
extern crate reqwest;
|
||||
|
||||
use serenity::client::{Client, EventHandler};
|
||||
use serenity::framework::standard::{StandardFramework, help_commands};
|
||||
use serenity::model::{channel::Message, id::ChannelId};
|
||||
use std::env;
|
||||
use std::fmt::Display;
|
||||
use regex::Regex;
|
||||
|
||||
struct Handler;
|
||||
|
||||
impl EventHandler for Handler {}
|
||||
|
||||
pub fn main() {
|
||||
dotenv::dotenv().ok();
|
||||
|
||||
// Load bot token from environment,
|
||||
let mut client = Client::new(&env::var("DISCORD_TOKEN").expect("token unavailable"), Handler)
|
||||
.expect("Error creating client");
|
||||
|
||||
client.with_framework(StandardFramework::new()
|
||||
.configure(|c|
|
||||
c
|
||||
.prefixes(vec!["++", "$", ">"])
|
||||
.allow_whitespace(true)
|
||||
.case_insensitivity(true)
|
||||
.on_mention(true))
|
||||
.help(help_commands::with_embeds)
|
||||
.command("ping", |c| c.cmd(ping).desc("Says Pong.").known_as("test"))
|
||||
.command("search", |c| c.cmd(search).desc("Executes a search using DuckDuckGo.").known_as("ddg"))
|
||||
.command("eval", |c| c.cmd(eval).desc("Evaluates an arithmetic expression.").known_as("calc"))
|
||||
.command("exec", |c| c.cmd(exec).desc("Executes code passed in codeblock with language set via Coliru. Supported languages: `python`, `shell`, `haskell`, `lua`."))
|
||||
.command("fortune", |c| c.cmd(fortune).desc("Displays a random `fortune`."))
|
||||
.command("eval-polish", |c| c.cmd(eval_polish).desc("Evaluates a Polish-notation arithmetic expression.")));
|
||||
|
||||
if let Err(why) = client.start() {
|
||||
eprintln!("An error occured: {:?}", why);
|
||||
}
|
||||
}
|
||||
|
||||
command!(ping(_context, message) {
|
||||
message.reply("Pong!")?;
|
||||
});
|
||||
|
||||
fn send_error(channel: &ChannelId, text: &str) -> std::result::Result<(), serenity::Error> {
|
||||
channel.send_message(|m| {
|
||||
m
|
||||
.embed(|e| e.title("Error").description(text).colour((255, 0, 0)))
|
||||
}).map(|_| ())
|
||||
}
|
||||
|
||||
fn send_text(channel: &ChannelId, text: &str) -> std::result::Result<(), serenity::Error> {
|
||||
channel.send_message(|m| {
|
||||
m
|
||||
.embed(|e| e.title("Result").description(text).colour((0, 255, 0)))
|
||||
}).map(|_| ())
|
||||
}
|
||||
|
||||
fn send_result<T: Display, E: Display>(message: &Message, res: &Result<T, E>) -> std::result::Result<(), serenity::Error> {
|
||||
match res {
|
||||
Ok(x) => send_text(&message.channel_id, &x.to_string()),
|
||||
Err(e) => send_error(&message.channel_id, &e.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Evaluate an arithmetic expression
|
||||
command!(eval(_context, message, args) {
|
||||
let expr = args.multiple::<String>()?.join(" "); // yes, this is kind of undoing the work the command parser does...
|
||||
send_result(message, &calc::eval(&expr))?;
|
||||
});
|
||||
|
||||
// Evaluate an arithmetic expression in polish notation
|
||||
command!(eval_polish(_context, message, args) {
|
||||
let expr = args.multiple::<String>()?.join(" ");
|
||||
send_result(message, &calc::eval_polish(&expr))?;
|
||||
});
|
||||
|
||||
fn execute_coliru(command: &str, code: &str) -> Result<String, reqwest::Error> {
|
||||
lazy_static! {
|
||||
static ref CLIENT: reqwest::Client = reqwest::Client::new();
|
||||
}
|
||||
|
||||
let mut data = std::collections::HashMap::new();
|
||||
data.insert("src", code);
|
||||
data.insert("cmd", command);
|
||||
|
||||
let mut res = CLIENT.post("http://coliru.stacked-crooked.com/compile")
|
||||
.json(&data)
|
||||
.send()?;
|
||||
|
||||
Ok(res.text()?)
|
||||
}
|
||||
|
||||
// Thanks StackOverflow!
|
||||
fn truncate(s: &str, max_chars: usize) -> &str {
|
||||
match s.char_indices().nth(max_chars) {
|
||||
None => s,
|
||||
Some((idx, _)) => &s[..idx],
|
||||
}
|
||||
}
|
||||
|
||||
fn to_code_block(s: &str) -> String {
|
||||
format!("```\n{}\n```", truncate(s, 1990)) // Discord only allows 2000 Unicode codepoints per message
|
||||
}
|
||||
|
||||
fn execute_and_respond(channel: &ChannelId, command: &str, code: &str) -> Result<(), serenity::Error> {
|
||||
channel.broadcast_typing()?;
|
||||
let coliru_result = execute_coliru(command, code);
|
||||
|
||||
match coliru_result {
|
||||
Ok(stdout) => {
|
||||
channel.send_message(|m|
|
||||
m.content(&to_code_block(&stdout)))?;
|
||||
},
|
||||
Err(e) => send_error(channel, &format!("{}", e))?
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
command!(exec(_context, message) {
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new("(?s)^.*exec.*```([a-zA-Z0-9_\\-+]+)\n(.+)```").unwrap();
|
||||
}
|
||||
|
||||
let captures = match RE.captures(&message.content) {
|
||||
Some(x) => x,
|
||||
// Presumably just returning the Result from send_error should work, but it doesn't.
|
||||
None => {
|
||||
send_error(&message.channel_id, r#"Invalid format; expected a codeblock with a language set."#)?;
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
|
||||
let code = &captures[2];
|
||||
let lang = captures[1].to_lowercase();
|
||||
let lang = lang.as_str();
|
||||
let channel = &message.channel_id;
|
||||
|
||||
match lang {
|
||||
"test" => execute_and_respond(channel, "echo Hello, World!", ""),
|
||||
"py" | "python" => execute_and_respond(channel, "mv main.cpp main.py && python main.py", code),
|
||||
"sh" | "shell" => execute_and_respond(channel, "mv main.cpp main.sh && sh main.sh", code),
|
||||
"lua" => execute_and_respond(channel, "mv main.cpp main.lua && lua main.lua", code),
|
||||
"haskell" | "hs" => execute_and_respond(channel, "mv main.cpp main.hs && runhaskell main.hs", code),
|
||||
_ => send_error(channel, &format!("Unknown language `{}`.", lang))
|
||||
}?;
|
||||
});
|
||||
|
||||
command!(fortune(_context, message) {
|
||||
let output = std::process::Command::new("fortune").output()?.stdout;
|
||||
let output = String::from_utf8(output)?;
|
||||
message.channel_id.send_message(|m| m.content(output))?;
|
||||
});
|
||||
|
||||
// BELOW THIS LINE BE DRAGONS
|
||||
|
||||
struct SearchResult {
|
||||
url: Option<String>,
|
||||
image: Option<String>,
|
||||
text: String,
|
||||
title: String
|
||||
}
|
||||
|
||||
fn send_search_result(channel: &ChannelId, res: SearchResult) -> std::result::Result<(), serenity::Error> {
|
||||
channel.send_message(|m| {
|
||||
m
|
||||
.embed(|e| {
|
||||
let e = e.title(res.title).description(res.text).colour((0, 255, 255));
|
||||
let e = match res.url {
|
||||
Some(u) => e.url(u),
|
||||
None => e
|
||||
};
|
||||
let e = match res.image {
|
||||
Some(u) => e.image(u),
|
||||
None => e
|
||||
};
|
||||
e
|
||||
})
|
||||
}).map(|_| ())
|
||||
}
|
||||
|
||||
fn none_if_empty(s: String) -> Option<String> {
|
||||
if s.len() == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_topics(t: ddg::RelatedTopic) -> Vec<ddg::response::TopicResult> {
|
||||
match t {
|
||||
ddg::RelatedTopic::TopicResult(t) => vec![t],
|
||||
ddg::RelatedTopic::Topic(t) => {
|
||||
let mut out = vec![];
|
||||
for subtopic in t.topics {
|
||||
out.append(&mut get_topics(subtopic))
|
||||
}
|
||||
out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
command!(search(_context, message, args) {
|
||||
let query = args.multiple::<String>()?.join(" ");
|
||||
let result = ddg::Query::new(query.as_str(), "autobotrobot").no_html().execute()?;
|
||||
let channel = &message.channel_id;
|
||||
|
||||
match result.response_type {
|
||||
ddg::Type::Article | ddg::Type::Name => send_search_result(channel, SearchResult {
|
||||
title: query,
|
||||
image: none_if_empty(result.image),
|
||||
text: result.abstract_text,
|
||||
url: none_if_empty(result.abstract_url)
|
||||
})?,
|
||||
ddg::Type::Disambiguation => {
|
||||
let mut results = vec![];
|
||||
for related_topic in result.related_topics {
|
||||
for topic in get_topics(related_topic) {
|
||||
results.push(topic);
|
||||
}
|
||||
}
|
||||
|
||||
for topic in results.drain(..5) {
|
||||
send_search_result(channel, SearchResult {
|
||||
url: none_if_empty(topic.first_url),
|
||||
image: none_if_empty(topic.icon.url),
|
||||
title: query.clone(),
|
||||
text: topic.text
|
||||
})?;
|
||||
}
|
||||
},
|
||||
ddg::Type::Exclusive => {
|
||||
send_search_result(channel, SearchResult {
|
||||
title: query,
|
||||
text: result.redirect.clone(),
|
||||
image: None,
|
||||
url: Some(result.redirect)
|
||||
})?
|
||||
},
|
||||
ddg::Type::Nothing => send_error(channel, "No results.")?,
|
||||
other => send_error(channel, &format!("{:?} - unrecognized result type", other))?
|
||||
}
|
||||
});
|
25
src/tio.py
Normal file
25
src/tio.py
Normal file
@ -0,0 +1,25 @@
|
||||
import pytio
|
||||
import http3
|
||||
import gzip
|
||||
import io
|
||||
|
||||
tio = pytio.Tio()
|
||||
|
||||
def languages():
|
||||
return tio.query_languages()
|
||||
|
||||
aliases = {
|
||||
"python": "python3"
|
||||
}
|
||||
|
||||
client = http3.AsyncClient()
|
||||
|
||||
async def run(lang, code):
|
||||
req = pytio.TioRequest(aliases.get(lang, lang), code)
|
||||
res = await client.post("https://tio.run/cgi-bin/run/api/", data=req.as_deflated_bytes(), timeout=65)
|
||||
content = res.content.decode("UTF-8")
|
||||
split = list(filter(lambda x: x != "\n" and x != "", content.split(content[:16])))
|
||||
if len(split) == 1:
|
||||
return False, split[0], None
|
||||
else:
|
||||
return True, split[0], split[1]
|
Loading…
x
Reference in New Issue
Block a user