Skip to content

Instantly share code, notes, and snippets.

@christopher-dG
Last active November 3, 2020 11:26
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save christopher-dG/2d9327a1b4b3b24323c115dc179f87a2 to your computer and use it in GitHub Desktop.
Save christopher-dG/2d9327a1b4b3b24323c115dc179f87a2 to your computer and use it in GitHub Desktop.
Discord.jl's doc bot
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