Giter Site home page Giter Site logo

arikawa's Introduction

arikawa

 Pipeline Status  Report Card      Godoc Reference  Examples         Discord Gophers  Hime Arikawa

A Golang library for the Discord API.

Library Highlights

  • More modularity with components divided up into independent packages, such as the API client and the Websocket Gateway being fully independent.
  • Clear separation of models: API and Gateway models are never mixed together so to not be confusing.
  • Extend and intercept Gateway events, allowing for use cases such as reading deleted messages.
  • Pluggable Gateway cache allows for custom caching implementations such as Redis, automatically falling back to the API if needed.
  • Typed Snowflakes make it much harder to accidentally use the wrong ID (e.g. it is impossible to use a channel ID as a message ID).
  • Working user account support, with much of them in ningen. Please do not use this for self-botting, as that is against Discord's ToS.

Examples

commands-hybrid is an alternative variant of commands, where the program permits being hosted either as a Gateway-based daemon or as a web server using the Interactions Webhook API.

Both examples demonstrate adding interaction commands into the bot as well as an example of routing those commands to be executed.

Simple bot example without any state. All it does is logging messages sent into the console. Run with BOT_TOKEN="TOKEN" go run .. This example only demonstrates the most simple needs; in most cases, bots should use the state or the bot router.

Note that Discord discourages use of bots that do not use the interactions API, meaning that this example should not be used for bots.

A slightly more complicated example. This bot uses a local state to cache everything, including messages. It detects when someone deletes a message, logging the content into the console.

This example demonstrates the PreHandler feature of the state library. PreHandler calls all handlers that are registered (separately from the session), calling them before the state is updated.

Note that Discord discourages use of bots that do not use the interactions API, meaning that this example should not be used for bots.

Bare Minimum Bot Example

The least amount of code recommended to have a bot that responds to a /ping.

package main

import (
	"context"
	"log"
	"os"

	"github.com/diamondburned/arikawa/v3/api"
	"github.com/diamondburned/arikawa/v3/api/cmdroute"
	"github.com/diamondburned/arikawa/v3/gateway"
	"github.com/diamondburned/arikawa/v3/state"
	"github.com/diamondburned/arikawa/v3/utils/json/option"
)

var commands = []api.CreateCommandData{{Name: "ping", Description: "Ping!"}}

func main() {
	r := cmdroute.NewRouter()
	r.AddFunc("ping", func(ctx context.Context, data cmdroute.CommandData) *api.InteractionResponseData {
		return &api.InteractionResponseData{Content: option.NewNullableString("Pong!")}
	})

	s := state.New("Bot " + os.Getenv("BOT_TOKEN"))
	s.AddInteractionHandler(r)
	s.AddIntents(gateway.IntentGuilds)

	if err := cmdroute.OverwriteCommands(s, commands); err != nil {
		log.Fatalln("cannot update commands:", err)
	}

	if err := s.Connect(context.TODO()); err != nil {
		log.Println("cannot connect:", err)
	}
}

Testing

The package includes integration tests that require $BOT_TOKEN. To run these tests, do:

export BOT_TOKEN="<BOT_TOKEN>"
go test -tags integration -race ./...

arikawa's People

Contributors

avdb13 avatar ayn2op avatar benbebop avatar chanbakjsd avatar constantoine avatar daniel-boman avatar dependabot[bot] avatar diamondburned avatar flysand7 avatar hhhapz avatar hi117 avatar itslychee avatar juby210 avatar karitham avatar ks129 avatar matthewpi avatar mavolin avatar p5nbtgip0r avatar patyhank avatar rarkness avatar samhza avatar sepruko avatar starshine-sys avatar svenwiltink avatar tadeokondrak avatar twoscott avatar tystuyfzand avatar vendicated avatar xsam avatar zbnf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

arikawa's Issues

fix(api): no retries on error

Regarding: httputil.Client.Request

Currently, Request retries on any HTTP Response status that is < 200 or > 299.
However, many errors won't get resolved by a retry, as only 5xx errors indicate a server error and any other status indicates either an error with the data that was sent, etc..
An exception to this is of course code 429 - too many requests which indicates rate limiting.
See also https://en.wikipedia.org/wiki/List_of_HTTP_status_codes.

I would propose to check for server or rate limit errors and retry only then, but let all others break the loop and get handled respectively.
This could be done by simply replacing the

if status = r.GetStatus(); status < 200 || status > 299 {
    continue
}

with

if status = r.GetStatus(); status == http.StatusTooManyRequests || status >= 500 {
    continue
}

.

If you create a branch for it, I can hand in a PR for it.

API: lift limits for getters

Implement automatic pagination for all XRange methods, similar to how their respective X methods to it.

Can hand in a PR for this.

Gateway: fix missing inheritance

Some gateway events wrap a discord type and not inherit it in a struct:

type UserUpdateEvent discord.User

instead of

type UserUpdateEvent struct {
    discord.User
}

Just wrapping the type does inherit the methods of it's underlying type, but not it's methods. So if one would want to get the mention of the updated user, on couldn't call userUpdate.Mention() as it is not inherited.
However, using a struct with discord.User as anonymous field, would allow UserUpdateEvent to inherit discord.User's fields but also it's methods.

Can hand in a PR for this.

API: payload for ModifyEmoji always empty

The payload sent by api.Client.ModifyEmoji is always empty, as the param struct is initialized with zero values, but the values passed to the method never get assigned to the payload.

I will create a PR fixing this.

Allow variadic or last-ManualParser arguments

As of right now, a string needs to be quoted to be one, and an argument can have too many inputs.

Example

Consider this code:

func (*Roles) Give(name string) {}

Given !roles give Linux, this code would run as expected: name = "Linux". However, when given !roles give Red Hat, the bot would consider Hat an extra argument and thus would error out. The user would have to do !roles give "Red Hat", which is not so user friendly.

Proposals

Accept variadic arguments

A handler should be able to declare as following:

func (*Roles) Give(name ...string) {}

In this case, !roles give Red Hat would return name = []string{"Red", "Hat"}. This should work in a pretty straightforward way.

Accept ManualParser as the last argument (trailing manual parser)

If for whatever reason a handler wants to parse trailing strings differently, it should be able to combine ManualParser with primitives or Parser-implementing arguments.

Take this code for example:

func (*Roles) Flagger(name string, vargs *FlagVariadicArgs) {}

// FlagVariadicArgs contain optional arguments
type FlagVariadicArgs struct {
	Folder string
	Arg1   bool
	Arg2   bool
}

func (fv *FlagVariadicArgs) ParseContent(c []string) error {
	for i := range c {
		switch i {
		case 0: // folder
			fv.Folder = c[i]
		case 1:
			fv.Arg1 = true
		case 2:
			fv.Arg2 = true
		}
	}
	return nil
}

In this very rough example, we were able to declare optional arguments in an explicit and clear way. This could be very useful in some niche cases.

State: add FetchSync option

When requesting a resource from the state, it attempts to fetch it from the store, and if not found there starts a request against Discord's API. This quickly becomes a problem of efficiency for methods such as Permissions, which, in theory, could trigger up to 3 API requests consecutively, which costs a lot of time.
To improve speed I propose to add a FetchSync field to the State struct (this could also be named FetchAsync, but I think speed is the preferred behavior in most cases, hence default to async behavior). This means a default state will fetch all field asynchronously, starting a goroutine for each individual request, if there are more than one resources requested within a state method. Only if explicitly set to true the old, slower behavior will be applied.

Alternatively, this could be made default, without a way to change it.

If approved, I can hand in a PR for this.

Discord: fix problems with the Invite struct

  1. The approximate_member_count field is missing ApproxMembers has a spelling mistake in its JSON name though.
  2. The comment for InviteMetadata, should also state, that VanityURL uses the Uses field
  3. ApproxPresences should be ApproximatePresences to concur with the field with the same name in Guild

Can hand in a PR for this.

API: remove missing CreateGroup method comment

According to the API create group dm was part of the now deprecated GameBridge SDK. There is, therefore, no point in adding such a method, hence the comment should be removed

Can hand in a PR.

Bot: Custom NameFlag handlers

Since some NameFlags would be either pretty tedious or straight up impossible to implement without making the code a mess, I propose a pluggable NameFlag handler API.

This would close #22.

Current Proposal

type NameFlagHandler interface {
	HandleEvent(v interface{}) error
	HandleMessage(g *gateway.MessageCreateEvent) error
}

type Handlers map[rune]NameFlagHandler

API: GuildsRange query fields don't get omitted

When using GuildsAfter which internally uses GuildsRange it uses 0 as value for before. However, before doesn't get omitted, as it should, but gets send with as part of the query, effectively requesting all guilds with an id < 0, which of course don't exist.
It is possible that discord ignores this (seems unlikely though), as I got these values when mocking the request, but this should get fixed one way or another.

Can hand in a PR.

API: Deprecate FormatEmojiAPI in favor of a simpler function

FormatEmojiAPI takes two parameters, the first one being optional, which is not very idiomatic.
Also FormatEmojiAPI doesn't even return a EmojiAPI type, but a string.
Also, if we're doing this APIEmoji might be a better name, since this is an emoji used by the API and not an API for emojis.
Therefore, I propose to either deprecate or delete FormatEmojiAPI in favor of two new functions:

func NewAPIEmojiFromGuildEmoji(id discord.Snowflake, name string) APIEmoji {
    return name+":"+id.String()
}

func NewAPIEmojiFromUnicode(emoji string) APIEmoji {
    return name
}

Additionally, APIEmoji should get a URLEncode() string method, that URL encodes the emoji for safe API usage.

Can hand in a PR for this

Data race: Gateway and Pacemaker on v0.0.13

Description

We don't know much about the trace yet, other than that it's involved in the Pacemaker.

Trace

WARNING: DATA RACE
Write at 0x00c01d5882c0 by goroutine 53:
  github.com/diamondburned/arikawa/gateway.HandleOP()
      /home/diamond/Scripts/arikawa/gateway/pacemaker.go:34 +0x8a6
  github.com/diamondburned/arikawa/gateway.HandleEvent()
      /home/diamond/Scripts/arikawa/gateway/op.go:105 +0x164
  github.com/diamondburned/arikawa/gateway.(*Gateway).handleWS()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:323 +0x2b6

Previous read at 0x00c01d5882c0 by goroutine 51:
  github.com/diamondburned/arikawa/gateway.(*Pacemaker).Dead()
      /home/diamond/Scripts/arikawa/gateway/pacemaker.go:47 +0x64
  github.com/diamondburned/arikawa/gateway.(*Pacemaker).start()
      /home/diamond/Scripts/arikawa/gateway/pacemaker.go:89 +0x2b1
  github.com/diamondburned/arikawa/gateway.(*Pacemaker).StartAsync.func1()
      /home/diamond/Scripts/arikawa/gateway/pacemaker.go:103 +0x46

Goroutine 53 (running) created at:
  github.com/diamondburned/arikawa/gateway.(*Gateway).start()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:284 +0x600
  github.com/diamondburned/arikawa/gateway.(*Gateway).Start()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:230 +0x3c
  github.com/diamondburned/arikawa/gateway.(*Gateway).Open()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:202 +0x1f9
  github.com/diamondburned/arikawa/session.(*Session).Open()
      /home/diamond/Scripts/arikawa/session/session.go:108 +0x54
  github.com/diamondburned/arikawa/bot.Start()
      /home/diamond/Scripts/arikawa/bot/ctx.go:115 +0x1fb
  main.main()
      /home/diamond/Scripts/nixie/main.go:56 +0x2ee

Goroutine 51 (running) created at:
  github.com/diamondburned/arikawa/gateway.(*Pacemaker).StartAsync()
      /home/diamond/Scripts/arikawa/gateway/pacemaker.go:102 +0xf8
  github.com/diamondburned/arikawa/gateway.(*Gateway).start()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:254 +0x42b
  github.com/diamondburned/arikawa/gateway.(*Gateway).Start()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:230 +0x3c
  github.com/diamondburned/arikawa/gateway.(*Gateway).Open()
      /home/diamond/Scripts/arikawa/gateway/gateway.go:202 +0x1f9
  github.com/diamondburned/arikawa/session.(*Session).Open()
      /home/diamond/Scripts/arikawa/session/session.go:108 +0x54
  github.com/diamondburned/arikawa/bot.Start()
      /home/diamond/Scripts/arikawa/bot/ctx.go:115 +0x1fb
  main.main()
      /home/diamond/Scripts/nixie/main.go:56 +0x2ee

Context abstractions around API and the state cache

Allow copying the API struct with a non-nil context, which would allow people to inject a Context in.

Example:

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

var err error

api := session.API.WithContext(ctx)
err = api.Guild(123123) // 500ms each request
err = api.Guild(123123)
err = api.Guild(123123)
err = api.Guild(123123) // cumulative: 2 seconds

if err != nil {
	log.Fatalln("Failed to fetch guild:", err) // context expires, errors
}

Since the state also has an API abstraction, it could also have a similar method:

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

cstate := state.WithContext(ctx)
cstate.Guild() // will use context with copied API instance.

API: fix multiple issues with ModifyIntegration

  1. ModifyIntegration doesn't send a JSON payload but attempts to encode the JSON payload in the request's query, which obviously wouldn't work
  2. As all fields are omittable, it should be considered to replace them with their option type
  3. ExpireBehavior is an enum

Can hand in a PR

API: rename GuildImageType to GuildImageStyle

I don't know if it's worth breaking the API over this, but since a lot of (breaking) changes have occurred in the past commits, this is probably the best time for this.

Discord's API docs refer to the type that is currently known as GuildImageType as style, so GuildImageStyle might be more appropriate.
See here.

Can hand in a PR for this.

Discord: Simplify AuditLogChange

The current implementation of AuditLogChange is very type unsafe and could be simplified on client-side.

Proposal

AuditLogChange get refactored into a

type AuditLogChange interface {
    Key() AuditLogChangeKey
    NewValue() interface{}
    OldValue() interface{}
}

Additionally, for every AuditLogChangeKey we create a respective audit log change struct, that implements AuditLogChange that is correctly typed.

Now the complicated part:
AuditLogEntry gets its own UnmarshalJSON method, as JSON won't be able to unmarshal into an interface. This method decides, depending on the key of the AuditLogChange, which struct should be used and correctly puts the typed data into it.

In order to compensate for future AuditLogChanges that aren't added right away, a FallbackChange struct will be created, that is the same as the current implementation of AuditLogChange.

I just realized that AuditLogChanges don't need any user generation, therefore, adapted proposal:

AuditLogChange gets refactored into:

type AuditLogChange interface {
    Key AuditLogChangeKey
    NewValue interface{}
    OldValue interface{}
}

This time AuditLogChange is going to implement UnmarshalJSON and take care of all custom types such as Hash.

I can hand in a PR for this.

Command aliases and custom checks

Command aliases

Add a way to add aliases to command. This should be done like this:

bot.AddAlias("command", "alias")

Custom checks

Add custom checks. Implementation should similar like aliases:

bot.AddCheck("command", CheckFunction)

Discord: incomplete Invite type

The Invite type seems to be unfinished in 2 regards:

  1. The Inviter field seems to be missing (See here for docs on this)
  2. There is an InviteMetadata struct with the additional Metadata that would be filled when fetching channel and guild invites, but it is unused, making Invite effectively lack those fields.

Can hand in a PR for this, just need to know if I should let Invite inherit InviteMeta like below, or if I should create a dedicated MetaInvite that has those fields and that is returned by ChannelInvites and GuildInvites.

Unecessary Pointers

Commit 619558e introduced the idea, that fields, not required by discord should be made nillable, to allow the developer to explicitly omit them, as go types cannot be nil unless they are pointers (or slices, maps etc.). While this may be advisable for bools and (regular) numbers, it makes no sense for discord.Snowflake.
A valid Snowflake will, for obvious reason, never be zero, so a simple omitempty should suffice, as the godoc of json.Unmarshal confirms.

The "omitempty" option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string.

Enforcing pointers on Snowflakes requires code like this, as there are no utility functions for this case, as there are for json.OptionInt etc.
And also why make something hard if it doesn't have to?

var s discord.Snowflake
s = 123

d := api.SomeStructWithASnowflakePointer{
    Snowflake: &s
}

Assumingly following the same idea, some slices also got pointerized, which makes also no sense, as slices can be nil by default, and creating pointers to them bears many disadvantages, like for ... range not working.

The same goes for json.OptionString which in theory makes sense, but is not required at all, as all string fields where OptionString is used (at least now, I haven't looked through the potentially affected fields), would never be empty, if chosen to use.

On a side note, I discovered that the ModifyGuildData.Name shouldn't have the omitempty tag. While it won't make any difference as the discord API will return an error anyways, it still help with clarity.

Replace WS library to gorilla/websocket

Preamble

As found out by someone on the Discord, nhooyr.io/websocket pulls down very heavy dependencies.

image

Proposal

Change the Websocket library. There are 2 prominent proposals: gorilla/websocket and gobwas/ws.

These libraries above don't have mutices to protect Writes, but we could reuse our current mutex for that.

gorilla/websocket does not depend on any external dependencies (as shown by its go.mod file), making it a pretty good alternative.

gobwas/ws on the other hand seem to be more memory-friendly, however does not use Go Modules, making it less of an ideal alternative.

Deprecating NameFlag

This is a pretty big change if it ever happens, since my bot relies on it. Here goes nothing.

Current Problem

The current problem with the NameFlag API is that it's very tedious to use and feels very out of place. Take for example this code:

func (r *Roles) AーReset(m *gateway.MessageCreateEvent) error {

There isn't a lot of functions that have a flag as a prefix for their name in Go, let alone a (Japanese dash) character. This function name is also very hard to type, and writing a function name like this basically involves copy-pasting the character around.

Another problem is that this API is not flexible at all. Current NameFlag functionalities are hard-coded, which not only make NameFlags useless, but also cluttered in the code.

With the introduction of the CanSetup interface, subcommands can implement it with the Setup(*bot.Subcommand) method, allowing access to the subcommand at runtime.

Proposal

Below is the pseudo-code example that I sketched out. It implements all current NameFlags with methods called in the Setup function.

func (cmds *Commands) Setup(sub *bot.Subcommand) {
	// Raw
    sub.SetName("GC", "GC")
	// AdminOnly
    sub.AddMiddleware("GC", middlewares.AdminOnly(cmds))
	// GuildOnly
    sub.AddMiddleware("Toggle", middlewares.GuildOnly(cmds))
    // Middleware for all handlers ("*" == "")
	sub.AddMiddleware("*", middleware.GuildOnly(cmds))
	// Hidden
    sub.Hide("GC")
	// Plumb to the first handler. (>1) == UB
    sub.Plumb()
}

Middlewares

The biggest advantage of this change, other than getting rid of NameFlags, is the middleware. This API would allow users to arbitrarily implement any sort of middlewares, not just for the entire subcommand, but also for individual handlers.

Error Handling

Since this Setup function is called at runtime, calls should panic with an error. External subcommands could prevent this by writing a test that would call a helper function to check the Setup handler.

Anti-proposal

The NameFlag API does have its advantages. Initially, it was made with the idea that all methods should have their attributes marked near them. There wasn't any place other than the method name.

This is still true even with the Setup API. Clearly, the Setup API is typically on the top of the file (or any place), while methods could be any other place.

The API could also be improved and made more flexible. Issue #23 proposes a NameFlag handler repository and interfaces for different handler implementations. However, NameFlag characters are limited, so this isn't a very sustainable idea either.

Conclusion

In conclusion, I personally support deprecating the NameFlag API in favor of the Setup API. I think the Setup API has a lot more room for additional features. The NameFlag API, limited by the number of (sane) characters, would not be very good.

API: create data structs for various API methods

Some API methods just take a lot of parameters and build a payload with that. However, this bears two problems:

  1. Method signatures get very long, and sometimes even invoking them becomes a two-line job
  2. Often some of the parameters are omittable. As go doesn't support function overloads, we can't create a bunch of methods for this that would cover every case, but we can use structs

Proposal

  1. All methods, that take three or more data arguments or have more than five total arguments, should use a XData struct.
  2. Methods, where the API allows omittable fields, should, except for a few sane exceptions, always use a XData struct, to prevent unidiomatic and ugly zero-values in function parameters.

Can hand in a PR this.

Discord: allow custom file extension for URL methods

Create a type ImageType string with these constants:

Auto = "auto"
JPEG = ".jpeg"
PNG = ".png"
WebP = ".webp"
GIF = ".gif"

and this method:

func (t ImageType) format(h Hash) string {
    if t == Auto {
        if strings.HasPrefix(h, "a_") {
            return string(h)+".gif"
        }
        
        return string(h)+".png"
    }

    return string(h)+string(t)
}

This could be applied to all XURL methods allowing for more flexibility, in two ways:

  1. Refactor all other methods and make them take an ImageType as argument.
  2. Add a XURLWithType(i ImageType) that takes this type as an argument.

Can hand in a PR for this.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.