2018-11-09 19:38:08 +00:00
#[ macro_use ] extern crate serenity ;
extern crate dotenv ;
extern crate calc ;
extern crate ddg ;
extern crate regex ;
#[ macro_use ] extern crate lazy_static ;
2018-11-11 10:03:25 +00:00
extern crate reqwest ;
2018-11-09 19:38:08 +00:00
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 )
2018-11-11 10:03:25 +00:00
. before ( | _context , message , _command | { println! ( " > {} " , message . content ) ; true } )
2018-11-09 19:38:08 +00:00
. 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 " ) )
2018-11-11 16:07:17 +00:00
. command ( " exec " , | c | c . cmd ( exec ) . desc ( " Executes code passed in codeblock with language set via Coliru. Supported languages: `python`, `shell`, `haskell`, `lua`. " ) )
2018-11-09 19:38:08 +00:00
. 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 ) ) ? ;
} ) ;
2018-11-11 10:03:25 +00:00
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 > {
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 ( ( ) )
}
2018-11-09 19:38:08 +00:00
command! ( exec ( _context , message ) {
lazy_static! {
2018-11-11 11:07:39 +00:00
static ref RE : Regex = Regex ::new ( " (?s)^.*exec.*```([a-zA-Z0-9_ \\ -+]+) \n (.+)``` " ) . unwrap ( ) ;
2018-11-09 19:38:08 +00:00
}
2018-11-10 22:58:22 +00:00
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 ] ;
2018-11-11 14:22:01 +00:00
let lang = captures [ 1 ] . to_lowercase ( ) ;
let lang = lang . as_str ( ) ;
2018-11-11 10:03:25 +00:00
let channel = & message . channel_id ;
2018-11-10 22:58:22 +00:00
match lang {
2018-11-11 10:03:25 +00:00
" 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 ) )
} ? ;
2018-11-09 19:38:08 +00:00
} ) ;
// 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 ;
2018-11-11 10:03:25 +00:00
2018-11-09 19:38:08 +00:00
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 = > {
for related_topic in result . related_topics {
for topic in get_topics ( related_topic ) {
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
} ) ? ;
}
}
} ,
2018-11-10 22:58:22 +00:00
ddg ::Type ::Exclusive = > {
send_search_result ( channel , SearchResult {
title : query ,
text : result . redirect . clone ( ) ,
image : None ,
url : Some ( result . redirect )
} ) ?
} ,
2018-11-09 19:38:08 +00:00
ddg ::Type ::Nothing = > send_error ( channel , " No results. " ) ? ,
other = > send_error ( channel , & format! ( " {:?} - unrecognized result type " , other ) ) ?
}
} ) ;