Last active
November 3, 2020 11:26
-
-
Save christopher-dG/2d9327a1b4b3b24323c115dc179f87a2 to your computer and use it in GitHub Desktop.
Discord.jl's doc bot
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using Discord | |
using REPL | |
const PREFIX = "julia> " # Cutesy Julia prompt. | |
const EMOTE = "<:julia:511658283090706453>" # Julia logo. | |
const COLOUR = 0x36393f # "Invisible" border for dark theme. | |
const CHRIS, JUAN = 0x0295605b65400000, 0x02a82393a1420002 | |
const ADMINS = [CHRIS, JUAN] | |
chris_dm = nothing | |
module Sandbox | |
using Discord | |
client() = c | |
end | |
# Determine whether or not a message should be forwarded. | |
function shouldforward(c::Client, e::MessageCreate) | |
!ismissing(e.message.author) && e.message.author.id == me(c).id && return false | |
ismissing(e.message.guild_id) && return false | |
startswith(e.message.content, PREFIX) && return false | |
occursin("julia", lowercase(e.message.content)) || return false | |
occursin(".might-be-super.fun", lowercase(e.message.content)) && return false | |
return true | |
end | |
# Link to the documentation. | |
function doclink_cmd(c::Client, m::Message) | |
reply(c, m, "<https://purgepj.github.io/Discord.jl/latest>") | |
end | |
# Link to the examples. | |
function examples_cmd(c::Client, m::Message) | |
reply(c, m, "<https://github.com/PurgePJ/Discord.jl/tree/master/examples>") | |
end | |
# Get some suggestions for a missed doc search. | |
function fallback_docstring(arg::String) | |
io = IOBuffer() | |
println(io, """Nothing called `$arg` was found. Try `apropos("$arg")`?""") | |
corrections = split(sprint(REPL.repl_corrections, arg), '\n') | |
if length(corrections) > 1 | |
print(io, "Corrections: ") | |
cors = split(corrections[2][19:end], ", ") | |
for cor in cors[1:end-1] | |
print(io, "`$cor`, ") | |
end | |
lasttwo = map(s -> "`$s`", split(cors[end], " or ")) | |
println(io, lasttwo[1], " or ", lasttwo[2]) | |
end | |
search = strip(sprint(REPL.repl_search, arg)) | |
if search !== "search:" | |
println(io, "Search: ", join(map(s -> "`$s`", split(search[9:end], ' ')), ", ")) | |
end | |
return strip(String(take!(io))) | |
end | |
# Get a docstring and format it for a Discord embed. | |
function docstring(arg::String) | |
bindings = Symbol.(split(arg, '.'; keepempty=false)) | |
binding = try | |
foldl(getfield, bindings; init=Discord) | |
catch | |
return fallback_docstring(string(bindings[end])) | |
end | |
doc = string(Base.Docs.doc(binding)) | |
doc = replace(doc, r"\[`(.+?)`\]\(@ref\)" => s"`\1`") # Ref links. | |
doc = replace(doc, r"### (.+)" => s"**\1**") # Small headers. | |
# TODO: Deal with a single pound without messing up code block comments. | |
doc = replace(doc, r"#{2} (.+)" => s"__**\1**__") # Big headers. | |
doc = replace(doc, r"^- " => "* ") # Bullet points. | |
doc = replace(doc, r"!!! (.+)" => s"**\1**") # Note/warn tags. | |
doc = replace(doc, "\n\n" => "\n") # Extra newlines. | |
doc = replace(doc, "```jldoctest" => "```julia") # Doctest code blocks | |
doc = replace(doc, "```julia-repl" => "```julia") # REPL code blocks. | |
# Replace non-annotated code blocks with Julia code blocks. | |
lines = split(doc, '\n'; keepempty=false) | |
local start = false | |
for (i, line) in enumerate(lines) | |
if startswith(line, "```") | |
start = !start | |
if start && line == "```" | |
lines[i] = "```julia" | |
end | |
end | |
end | |
return join(lines, '\n') | |
end | |
# Reply to a message with a docstring. | |
function docstring_cmd(c::Client, m::Message, arg::String) | |
chunks = split_message(docstring(arg)) | |
if length(chunks) == 1 | |
reply(c, m, Embed(; title="$EMOTE ?$arg", description=chunks[1], color=COLOUR)) | |
else | |
len = length(chunks) | |
for (i, chunk) in enumerate(chunks) | |
wait(reply(c, m, Embed(; | |
title="$EMOTE ?$arg ($i/$len)", | |
description=chunk, | |
color=COLOUR, | |
))) | |
end | |
end | |
end | |
# Reply to a message with apropos output. | |
function apropos_cmd(c::Client, m::Message, token::String) | |
results = split(sprint(REPL.apropos, token), '\n'; keepempty=false) | |
if isempty(results) | |
reply(c, m, "No results.") | |
return | |
end | |
reply(c, m, Embed(; | |
title="""$EMOTE apropos("$token")""", | |
description=join(map(r -> "* `$r`", results), '\n'), | |
footer=EmbedFooter(; text="$(length(results)) result(s)"), | |
color=COLOUR, | |
)) | |
end | |
# Format something as a Julia code block. | |
function codeblock(@nospecialize val) | |
s = val === nothing ? "nothing" : string(val) | |
return "```julia\n$s\n```" | |
end | |
# Parse code into an expression. | |
function parsecode(code::String) | |
occursin("token", code) && error("tried to get token") | |
return try | |
Meta.parse("begin $code end") | |
catch e | |
:(sprint(showerror, $e)) | |
end | |
end | |
# Evaluate an expression and reply with the result. | |
function eval_cmd(c::Client, m::Message, code::Expr) | |
result = try | |
@eval Sandbox $code | |
catch e | |
sprint(showerror, e) | |
end | |
msg = codeblock(result) | |
occursin(lowercase(c.token[5:end]), lowercase(msg)) || reply(c, m, codeblock(result)) | |
end | |
# Set up a Client. | |
function client(token::String, prefix::String) | |
c = Client(token; prefix=prefix, presence=(game=(name=prefix, type=AT_LISTENING),),) | |
# Inject the client into the eval module. | |
@eval Sandbox const c = $c | |
# Don't cache anything, we don't need any storage. | |
disable_cache!(c) | |
# Only process commands. This is pretty hacky, I don't recommend it. | |
empty!(c.handlers) | |
add_handler!(c, Union{Ready, Resumed}, Discord.Defaults.mandatory) | |
# Register "julia" as a highlight word for just me. | |
# TODO: count=1 when that bug is fixed. | |
add_handler!(c, Ready; tag=:createdm, count=2) do c, e | |
global chris_dm = fetchval(create_dm(c; recipient_id=CHRIS)) | |
end | |
add_handler!(c, MessageCreate; predicate=shouldforward) do c, e | |
m = e.message | |
content = "<https://discordapp.com/channels/$(m.guild_id)/$(m.channel_id)/$(m.id)>" | |
create(c, Message, chris_dm; content=content) | |
end | |
# Add commands. | |
add_help!(c) | |
add_command!( | |
c, :docs, doclink_cmd; | |
help="Link to the documentation", | |
compile=true, | |
content=string(PREFIX, "docs"), | |
) | |
add_command!( | |
c, :examples, examples_cmd; | |
help="Link to the examples", | |
compile=true, | |
content=string(PREFIX, "examples"), | |
) | |
add_command!( | |
c, :?, docstring_cmd; | |
help="Get a docstring --- ?foo", | |
parsers=[identity], | |
pattern=r"^\?([^ ]+)", | |
compile=true, | |
content=string(prefix, "Discord.Client"), | |
) | |
add_command!( | |
c, :apropos, apropos_cmd, | |
help="""Search documentation --- apropos("foo")""", | |
parsers=[identity], | |
pattern=r"""apropos\("(.+?)"\)""", | |
compile=true, | |
content=string(prefix, """apropos("foo")"""), | |
) | |
add_command!( | |
c, :eval, eval_cmd; | |
parsers=[parsecode], | |
pattern=r"^```(?:julia)?\n(.*)\n```", | |
help="Evaluate a Julia code block (restricted)", | |
allowed=ADMINS, | |
) | |
return c | |
end | |
if abspath(PROGRAM_FILE) == @__FILE__ | |
c = client(ENV["DISCORD_TOKEN"], PREFIX) | |
while true | |
open(c) | |
wait(c) | |
isopen(c) && close(c) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment