Skip to content

Commit

Permalink
V0.5.1-rc.1 (#34)
Browse files Browse the repository at this point in the history
* Buttons (#1)

* Update running handle to allow for components

* Add component utilities

* Replace unnecessary info with debugs, remake request preparation async.

* Added time to debug logs

* Update README.md with example

* Add important line in example

* Bump ver

* Bug patch and docs (#33)

* Document obtain function

* Fix not responding to first command used

* Bump ver
  • Loading branch information
xxxAnn authored Jan 29, 2022
1 parent 30a4aaf commit ee09900
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 33 deletions.
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "Dizkord"
uuid = "c8b112a7-7677-4d35-8651-7c5b5cba290e"
authors = ["Kyando <izawa.iori.tan@gmail.com>"]
version = "0.4.0"
version = "0.5.1"

[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
Expand Down
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,29 @@ It can be added from the Git repository with the following command:
] add https://github.com/Humans-of-Julia/Dizkord.jl
```

# Example

```julia
# Discord Token and Application ID should be saved in Env vars
client = Client(
ENV["DISCORD_TOKEN"],
# For compat
ENV["APPLICATION_ID"] isa Number ? ENV["APPLICATION_ID"] : parse(UInt, ENV["APPLICATION_ID"]),
intents(GUILDS, GUILD_MESSAGES)
)

# Guild to register the command in
TESTGUILD = ENV["TESTGUILD"] isa Number ? ENV["TESTGUILD"] : parse(Int, ENV["TESTGUILD"])

command!(client, TESTGUILD, "boom", "Go boom!") do (ctx)
Dizkord.reply(client, ctx, content="<@$(ctx.interaction.member.user.id)> blew up!")
end

on_ready!(client) do (ctx)
@info "Successfully logged in as $(ctx.user.username)"
end

start(client)
```

Big thanks to [@Xh4H](https://github.com/Xh4H) for Discord.jl which this relied heavily on.
2 changes: 1 addition & 1 deletion src/Dizkord.jl
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ using OpenTrick
using Setfield
using TimeToLive

const DISCORD_JL_VERSION = v"0.2.0"
const DISCORD_JL_VERSION = v"0.5.1"
const API_VERSION = 9
const DISCORD_API = "https://discordapp.com/api"

Expand Down
35 changes: 23 additions & 12 deletions src/client/handlers.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export on_message!,
on_ready!,
command!
command!,
component!,
component

"""
on_message!(
Expand Down Expand Up @@ -53,6 +55,10 @@ function command!(f::Function, c::Client, g::Number, name::AbstractString, descr
add_command!(c, Snowflake(g); name=name, description=description, kwargs...)
end

function component!(f::Function, c::Client, custom_id::AbstractString; kwargs...)
add_handler!(c, OnInteractionCreate(f; custom_id=custom_id))
return Component(custom_id=custom_id; kwargs...)
end
"""
handle(
c::Client
Expand All @@ -64,19 +70,24 @@ Creates an `AbstractContext` based on the event using the `data` provided and pa
"""
function handle(c::Client, handlers::Vector{Handler}, data::Dict, t::Symbol)
ctx = context(t, data::Dict)
for h = handlers
if !hasproperty(h, :name) || h.name == ctx.interaction.data.name
f = h.f
@spawn begin
try
f(ctx)
catch err
@error "Running handlers unexpectedly errored" event=String(t) error=err
end
end
end
isvalidcommand = (h) -> return (!hasproperty(h, :name) || (!ismissing(ctx.interaction.data.name) && h.name == ctx.interaction.data.name))
isvalidcomponent = (h) -> return (!hasproperty(h, :custom_id) || (!ismissing(ctx.interaction.data.custom_id) && h.custom_id == ctx.interaction.data.custom_id))
isvalid = (h) -> return isvalidcommand(h) && isvalidcomponent(h)
for hh in handlers
isvalid(hh) && (runhandler(c, hh, ctx, t))
end
end

"""
Runs a handler with given context
"""
function runhandler(c::Client, h::Handler, ctx::Context, t::Symbol)
if hasproperty(h, :name) || hasproperty(h, :custom_id)
ack_interaction(c, ctx.interaction.id, ctx.interaction.token)
end
h.f(ctx)
end

"""
Calls handle with a list of all found handlers.
"""
Expand Down
8 changes: 7 additions & 1 deletion src/rest/crud/crud.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ create(c, Ban, guild, member; reason="baz")
function create end

"""
retrieve(c::Client, ::Type{T}, args...; kwargs...) -> Future{Response}
retrieve(c::Client, ::Type{T}, args...; kwargs...) -> Future{Response{T}}
Retrieve, get, list, etc.
Expand All @@ -46,7 +46,13 @@ retrieve(c, Invite, "abcdef")
"""
function retrieve end

"""
obtain(c::Client, ::Type{T}, args...; kwargs...) -> T
Equivalent to retrieve, but blocks and returns the object of type T
"""
obtain(args...; kwargs...) = fetch(retrieve(args...; kwargs...)).val

"""
update(c::Client, x::T, args...; kwargs...) -> Future{Response}
Expand Down
13 changes: 12 additions & 1 deletion src/rest/endpoints/interaction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,25 @@ function get_application_commands(c::Client, guild::Snowflake)
return Response{Vector{ApplicationCommand}}(c, :GET, "/applications/$appid/guilds/$guild/commands")
end

function create_followup_message(c::Client, int_id::Snowflake, int_token::String; kwargs...)
function respond_to_interaction(c::Client, int_id::Snowflake, int_token::String; kwargs...)
dict = Dict{Symbol, Any}(
:data => kwargs,
:type => 4,
)
return Response{Message}(c, :POST, "/interactions/$int_id/$int_token/callback"; body=dict)
end

function create_followup_message(c::Client, int_id::Snowflake, int_token::String; kwargs...)
appid = c.application_id
return Response(c, :POST, "/webhooks/$appid/$int_token)"; body=kwargs)
end

function ack_interaction(c::Client, int_id::Snowflake, int_token::String; kwargs...)
dict = Dict{Symbol, Any}(
:type => 5,
)
return Response(c, :POST, "/interactions/$int_id/$int_token/callback"; body=dict)
end
function bulk_overwrite_application_commands(c::Client, guild::Snowflake, cmds::Vector{ApplicationCommand})
appid = c.application_id
return Response{Vector{ApplicationCommand}}(c, :PUT, "/applications/$appid/guilds/$guild/commands"; body=cmds)
Expand Down
28 changes: 17 additions & 11 deletions src/rest/rest.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,19 @@ end

# HTTP response with body (maybe).
function Response{T}(c::Client, r::HTTP.Messages.Response) where T
if T == Nothing
return Response{Nothing}(r)
end
r.status == 204 && return Response{T}(nothing, true, r, nothing)
r.status >= 300 && return Response{T}(nothing, false, r, nothing)

data = if HTTP.header(r, "Content-Type") == "application/json"
symbolize(JSON3.read(String(copy(r.body))))
val = if HTTP.header(r, "Content-Type") == "application/json"
symbolize(JSON3.read(String(copy(r.body)), T))
else
copy(r.body)
end

val, e = tryparse(c, T, data)
return Response{T}(val, e === nothing, r, e)
return Response{T}(val, true, r, nothing)
end

# HTTP request with no expected response body.
Expand All @@ -76,8 +78,7 @@ function Response{T}(
kwargs...
) where T
f = Future()

@async begin
@spawn begin
kws = (
channel=cap("channels", endpoint),
guild=cap("guilds", endpoint),
Expand All @@ -86,7 +87,7 @@ function Response{T}(
)

# Retrieve the value from the cache, maybe.
@debug "Looking in cache"
@debug "Preparing Request: Looking in cache" Time=now()
if c.use_cache && method === :GET
val = get(c.state, T; kws...)
if val !== nothing
Expand All @@ -103,19 +104,20 @@ function Response{T}(
end
# Prepare the request.
url = "$DISCORD_API/v$(c.version)$endpoint"
@debug "Did not find in cache, preparing request"
@debug "Preparing Request: Did not find in cache, preparing request" Time=now()
isempty(kwargs) || (url *= "?" * HTTP.escapeuri(kwargs))
headers = merge(Dict(
"User-Agent" => "Discord.jl $DISCORD_JL_VERSION",
"Content-Type" => "application/json",
"Authorization" => c.token,
), Dict(headers))
args = [method, url, headers]
@debug "Preparing Request: Type of request: $T" Time=now()
if get(SHOULD_SEND, method, false)
push!(args, headers["Content-Type"] == "application/json" ? JSON3.write(body) : body)
end

@debug "About to send request"
@debug "Preparing Request: Enqueuing request" Time=now()
# Queue a job to be run within the rate-limiting constraints.
enqueue!(c.limiter, method, endpoint) do
@debug "Enqueued job running"
Expand All @@ -125,6 +127,10 @@ function Response{T}(
# Make an HTTP request, and generate a Response.
# If we hit an upstream rate limit, return the response immediately.
http_r = HTTP.request(args...; status_exception=false)
@debug "Sent http request" Time=now()
if http_r.status - 200 >= 100
@warn "Got an unexpected response" Code=http_r.status Body=http_r Sent=args
end
http_r.status == 429 && return http_r
r = Response{T}(c, http_r)

Expand All @@ -143,11 +149,11 @@ function Response{T}(
put!(f, Response{T}(nothing, false, http_r, e))
end

@debug "Returning internally" response=http_r url=args[2]
@debug "Request completed" url=args[2]
return http_r
end
end
@debug "Returning future"
@debug "Returning future" Time=now()
return f
end

Expand Down
40 changes: 37 additions & 3 deletions src/types/interaction.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,48 @@ struct ApplicationCommand <: DiscordObject
end
@boilerplate ApplicationCommand :constructors :docs :lower :merge :mock

"""
A select option.
More details [here](https://discord.com/developers/docs/interactions/message-components#select-menu-object-select-option-structure).
"""
struct SelectOption <: DiscordObject
label::String
value::String
description::Optional{String}
emoji::Optional{Emoji}
default::Optional{Bool}
end
@boilerplate SelectOption :constructors :docs :lower :merge :mock

"""
An interactable component.
More details [here](https://discord.com/developers/docs/interactions/message-components).
"""
struct Component <: DiscordObject
type::Int
custom_id::Optional{String}
disabled::Optional{Bool}
style::Optional{Int}
label::Optional{String}
emoji::Optional{Emoji}
url::Optional{String}
options::Optional{Vector{SelectOption}}
placeholder::Optional{String}
min_values::Optional{Int}
max_values::Optional{Int}
components::Optional{Vector{Component}}
end
@boilerplate Component :constructors :docs :lower :merge :mock


"""
Data for an interaction.
More details [here](https://discord.com/developers/docs/interactions/receiving-and-responding#interaction-object-interaction-data-structure).
"""
struct InteractionData <: DiscordObject
id::Nullable{Snowflake}
name::String
type::Int
id::OptionalNullable{Snowflake}
name::OptionalNullable{String}
type::OptionalNullable{Int}
resolved::Optional{ResolvedData}
options::Optional{Vector{ApplicationCommandOption}}
custom_id::OptionalNullable{String}
Expand Down
7 changes: 7 additions & 0 deletions src/types/types.jl
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ macro constructors(T)
$(esc(T))(d::Dict{Symbol, Any}) = $(esc(T))(; d...)
$(esc(T))(d::Dict{String, Any}) = $(esc(T))(symbolize(d))
$(esc(T))(x::$(esc(T))) = x
function StructTypes.keyvaluepairs(x::$(esc(T)))
d = Dict{Symbol, Any}()
for k = fieldnames(typeof(x))
d[k] = getfield(x, k)
end
d
end
StructTypes.StructType(::Type{$(esc(T))}) = StructTypes.DictType()
end
end
Expand Down
1 change: 1 addition & 0 deletions src/utils/helpers.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export PERM_NONE,
set_game,
opt,
extops,
component,
@fetch,
@fetchval,
@deferred_fetch,
Expand Down
13 changes: 10 additions & 3 deletions test/fulltest.jl
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ client = Client(
intents(GUILDS, GUILD_MESSAGES)
)

ENV["JULIA_DEBUG"] = Dizkord

TESTGUILD = ENV["TESTGUILD"] isa Number ? ENV["TESTGUILD"] : parse(Int, ENV["TESTGUILD"])

on_message!(client) do (ctx)
Expand All @@ -23,8 +25,13 @@ command!(client, TESTGUILD, "bam", "Go bam!") do (ctx)
Dizkord.reply(client, ctx, content="<@$(ctx.interaction.member.user.id)> slapped themselves!")
end

command!(client, TESTGUILD, "water", "Water the plants!"; options=[opt(name="someop", description="Dosum")]) do (ctx)
Dizkord.reply(client, ctx, content="<@$(ctx.interaction.member.user.id)> watered the plant so much they grew taller than them!\n$(opt(ctx))")
function ff(ctx)
Dizkord.reply(client, ctx, content="You pressed the button!!")
end

command!(client, TESTGUILD, "water", "Water a plant"; options=[opt(name="howmuch", description="How long do you want to water the plant?")]) do (ctx)
cm = component!(ff, client, "magic"; type=2, style=1, label="Wow, a Button!?")
Dizkord.reply(client, ctx, components=[Dizkord.Component(; type=1, components=[cm])], content="<@$(ctx.interaction.member.user.id)> watered their plant for $(opt(ctx)["howmuch"]) hours. So much that the plant grew taller than them!")
end

command!(client, TESTGUILD, "quit", "Ends the bot process!") do (ctx)
Expand All @@ -33,7 +40,7 @@ command!(client, TESTGUILD, "quit", "Ends the bot process!") do (ctx)
end

on_ready!(client) do (ctx)
@info "Successfully logged in as $(ctx.user.username)"
@info "Successfully logged in as $(ctx.user.username)" # obtain(client, User).username
end

start(client)

0 comments on commit ee09900

Please sign in to comment.