{"id":13410880,"url":"https://github.com/passcod/accord","last_synced_at":"2025-03-14T16:33:13.818Z","repository":{"id":52938759,"uuid":"282514658","full_name":"passcod/accord","owner":"passcod","description":"Discord API client to power Discord API clients via the power of love, friendship, and HTTP 💖","archived":true,"fork":false,"pushed_at":"2022-10-08T17:47:34.000Z","size":327,"stargazers_count":16,"open_issues_count":0,"forks_count":3,"subscribers_count":3,"default_branch":"main","last_synced_at":"2024-07-31T20:44:15.890Z","etag":null,"topics":["caretaker","discord","discord-api","http","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/passcod.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-25T19:50:38.000Z","updated_at":"2023-11-17T09:09:28.000Z","dependencies_parsed_at":"2023-01-19T15:30:43.551Z","dependency_job_id":null,"html_url":"https://github.com/passcod/accord","commit_stats":null,"previous_names":[],"tags_count":10,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/passcod%2Faccord","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/passcod%2Faccord/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/passcod%2Faccord/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/passcod%2Faccord/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/passcod","download_url":"https://codeload.github.com/passcod/accord/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":221486913,"owners_count":16830966,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["caretaker","discord","discord-api","http","rust"],"created_at":"2024-07-30T20:01:09.975Z","updated_at":"2024-10-26T02:30:23.888Z","avatar_url":"https://github.com/passcod.png","language":"Rust","readme":"### Archived: was made before slash commands, now rather outdated. Idea was interesting and it did power at least one bot for ~2 years, but it's time to retire it.\n\n[![Crate release version](https://flat.badgen.net/crates/v/passcod-accord)](https://crates.io/crates/passcod-accord)\n[![Crate license: CC BY-NC-SA 4.0](https://flat.badgen.net/badge/license/CC%20BY-NC-SA%204.0)](./LICENSE)\n![MSRV: latest stable](https://flat.badgen.net/badge/MSRV/latest%20stable/orange)\n[![Uses Caretaker Maintainership](https://flat.badgen.net/badge/Caretaker/Maintainership%20👥%20/purple)](https://gist.github.com/passcod/7332390db1813f9bccb07e5cf3a9649b)\n\n# ![Accord: interfaces between discord and a local http server](./res/pitch.png)\n\n- Status:\n  + alpha\n  + in production for my use only\n  + covers only a [small](https://github.com/passcod/accord/issues/1) part of the API\n- Releases:\n  + see the [releases tab](https://github.com/passcod/accord/releases) for tagged releases\n  + no pre-built binaries yet, build from source\n  + or with `cargo install passcod-accord --locked`\n- License: [CC-BY-NC-SA 4.0](./LICENSE)\n  + “Uhhh... this isn't a software license?”\n    * Indeed. It still functions as a “work” license.\n  + It's not open source!\n    * Yes, this is by design.\n  + What if I want to use it in a commercial context?\n    * [See the top of the LICENSE file](./LICENSE)\n- Contribute: you can!\n  + This project uses [Caretaker Maintainership](./CARETAKERS.md).\n  + Areas in need of love: everywhere.\n  + More descriptive erroring and warnings would help lots!\n  + Basic response timing stats could be helpful!\n  + Anywhere there's a TODO comment...\n  + Some example applications would be ace!\n  + And of course, handling of more events is most welcome.\n\n## Docs\n\nTo get started, stand up a server (for example, a PHP standalone server that\nroutes everything to `index.php`: `php -S 127.0.0.1:8080 index.php`) and add\nits address to the `ACCORD_TARGET` environment variable.\n\nThen add your bot's discord token to `DISCORD_TOKEN`, and start Accord.\n\nAccord will now make a request to your server whenever an event occurs on\nDiscord that your bot can see.\n\nCaveat (to be resolved): your bot currently needs to have the Members\n[privileged intent][privileged] enabled. This will become configurable later.\n\n[privileged]: https://discord.com/developers/docs/topics/gateway#privileged-intents\n\n### Configuration\n\nDone through environment variables.\n\n| Name | Default | Purpose | Example |\n|------|---------|---------|---------|\n| `DISCORD_TOKEN` | **required** | Discord app token. ||\n| `ACCORD_TARGET` | **required** | Base URL of the server to send Accord requests to. | `http://localhost:8080` |\n| `ACCORD_BIND` | `localhost:8181` | Address to bind the reverse interface to. | `0.0.0.0:1234` |\n| [`ACCORD_COMMAND_MATCH`](#commands) | _none_ | Regex run on messages to match (true/false) as commands. | `^~\\w+` |\n| [`ACCORD_COMMAND_PARSE`](#commands) | _none_ | Regex run on commands to parse them out (with captures). | `(?:^~\\|\\s+)(\\w+)` |\n| `RUST_LOG` | `info` | Sets the log level. See [tracing](https://docs.rs/tracing-subscriber/0.2/tracing_subscriber/filter/struct.EnvFilter.html). | `info,accord=debug` |\n\n### Events to endpoint table\n\n| Event | Endpoint | Payload type | Responses allowed |\n|-------|----------|--------------|-------------------|\n| `MessageCreate` (from a guild) | `POST /server/{guild-id}/channel/{channel-id}/message` | [`Message`](#payload-type-message) | [`text/plain` reply content](#response-text-reply), [`application/json` acts](#response-json-acts) |\n| `MessageCreate` (from a DM) | `POST /direct/{channel-id}/message` | [`Message`](#payload-type-message) | [`text/plain` reply content](#response-text-reply), [`application/json` acts](#response-json-acts) |\n| `MessageCreate` (matching command regex) | `POST /command/{command...}` | [`Command`](#payload-type-command) | [`text/plain` reply content](#response-text-reply), [`application/json` acts](#response-json-acts) |\n| `MemberAdd` | `POST /server/{guild-id}/join/{user-id}` | [`Member`](#payload-type-member) | [`application/json` acts](#response-json-acts) |\n| `ShardConnected` | `POST /discord/connected` | [`Connected`](#payload-type-connected) | [`application/json` acts](#response-json-acts) |\n| _before a connection is made_ | `GET /discord/connecting` | none | [`application/json` presence](#response-json-presence) |\n\n### Payloads\n\nAll non-GET endpoint requests carry a payload, which is a JSON value of\nwhatever particular type the event generates (see the table). Some types have\nsubtypes, and so on. Types are given here in Typescript notation:\n\n#### Payload type: `Message`\n\n```typescript\n{\n  id: number, // u64\n  server_id?: number, // always present for guild messages, never for DMs\n  channel_id: number,\n  author: Member | User, // Member for guild messages, User for DMs\n\n  timestamp_created: string, // as provided from discord\n  timestamp_edited?: string, // as provided from discord\n\n  kind?: \"regular\", // usually \"regular\" (default), see source for others\n  content: string,\n\n  attachments: Array\u003cAttachment\u003e, // from twilight, type not stable/documented\n  embeds: Array\u003cEmbed\u003e, // idem\n  reactions: Array\u003cMessageReaction\u003e, // idem\n\n  application?: MessageApplication, // idem\n  flags: Array\u003c\"crossposted\" | \"is-crosspost\" | \"suppress-embeds\" | \"source-message-deleted\" | \"urgent\"\u003e,\n}\n```\n\n#### Payload type: `Member`\n\n```typescript\n{\n  user: User,\n  server_id: number,\n  roles?: Array\u003cnumber\u003e, // IDs of the roles\n  pseudonym?: string, // Aka the \"server nick\"\n}\n```\n\n#### Payload type: `User`\n\n```typescript\n{\n  id: number, // u64\n  name: string,\n  bot: boolean,\n}\n```\n\n#### Payload type: `Connected`\n\n```typescript\n{\n  shard: number,\n}\n```\n\n#### Payload type: `Command`\n\n```typescript\n{\n  command: Array\u003cstring\u003e, // captures from the ACCORD_COMMAND_PARSE regex\n  message: Message,\n}\n```\n\n### Headers\n\nThere are a set of headers, all beginning by `accord-`, that are set by events.\nAll the information in headers is also available in the payload (except for the\n`accord-version` header, which is present on all requests but in no payload),\nand these are intended less for the application (which should parse the payload\ninstead) and more for the request router (which might not posses the ability to\ninspect bodies or parse JSON). For example, nginx could route DM events\n(`accord-channel-type: direct`) to a different application.\n\n - `accord-version` — Always provided, the version of Accord itself;\n - `accord-server-id` — In guild context only;\n - `accord-channel-id` — In channel contexts;\n - `accord-channel-type` — `text` or `voice` in guilds, `direct` for DMs.\n - `accord-message-id` — In message contexts;\n - `accord-author-type` or `accord-user-type` — `bot` or `user`;\n - `accord-author-id` or `accord-user-id`;\n - `accord-author-name` or `accord-user-name`;\n - `accord-author-role-ids` or `accord-user-role-ids`;\n - `accord-content-length` — In message contexts, the length of the message.\n\n### Statuses\n\nThe response status code is handled identically throughout:\n\n - 1xx are not supported unless curl handles them internally;\n - 204 and 404 abort reading the response and return without any further action;\n - multiple-choice (300) is not supported (but may be in future);\n - not-modified (304) is not supported _yet_;\n - redirects are handled internally by curl (limit 8);\n - proxy redirections (305 and 306) are unsupported;\n - error statuses (400 and above) log an error, and may do more later;\n - all other success statuses are interpreted as a 200, and handling continues as below:\n\n### Responses\n\nThe response expected from any endpoint varies. Generally the body needs to be\nJSON, but there are some endpoints that accept other types, like text, for\nconvenience.\n\nThe general JSON response format is called \"act\" and represents a single action\nto be taken by Accord. An act is an object with one key describing its type,\nand that particular act's properties as a child object.\n\nThe `content-type` header of the response must be `application/json` for that\nformat, and the JSON must contain no literal newlines (i.e. it can't be\n\"pretty\" JSON).\n\nMessage create and command endpoints accept a `content-type: text/plain`\nresponse and interpret it as a `message-create` act with the response's body as\ncontent.\n\nMultiple actions are possible with the JSON format by separating each act with\na newline (which is why individual acts can't span more than one line). An\nempty line is ignored without error. Each line is parsed as an act and actioned\nas soon as it is received, and the connection is kept open until EOL is\nreceived, so you can stream multiple acts with arbitrary delays in-between, and\nsend \"keepalives\" to make sure the connection stays open in the form of\nadditional newlines. Lines are trimmed of leading and trailing whitespace\nbefore parsing as JSON, so you can pad out your messages to ~4096 bytes to\nreach buffering thresholds.\n\n(For this reason, on top of simple performance concerns, your server _must_\nsupport multiple simultaneous connections.)\n\nA few endpoints have special formats and do not support JSON act.\n\n#### Response: JSON acts\n\n##### Act: `create-message`\n\nPosts a new message.\n\n```typescript\n{ \"create-message\": {\n  content: string,\n  channel_id?: number, // u64 channel id to post in\n} }\n```\n\nThe `content` is internally converted to UTF-16 codepoints and cannot exceed\n2000 of them (this is a Discord limit).\n\nChannel IDs are globally unique, so there's no need to supply a server ID.\nAccord will attempt to fill in the channel ID if not present in the act. In\norder of precedence:\n\n - the act's `channel_id`\n - the response header `accord-channel-id`, if present\n - if the request is from a message context, that message's channel\n\n##### Act: `assign-role`\n\nAssigns a role to a member.\n\n```typescript\n{ \"assign-role\": {\n  role_id: number,\n  user_id: number,\n  server_id?: number,\n  reason?: string,\n} }\n```\n\nAccord will attempt to fill in the server ID if not present in the act. In\norder of precedence:\n\n - the act's `server_id`\n - the response header `accord-server-id`, if present\n - if the request is from a guild context, that guild\n\nThe `reason` string, when given, shows up in the guild's audit log.\n\n##### Act: `remove-role`\n\nRemoves a role from a member.\n\n```typescript\n{ \"assign-role\": {\n  role_id: number,\n  user_id: number,\n  server_id?: number,\n  reason?: string,\n} }\n```\n\nAccord will attempt to fill in the server ID if not present in the act. In\norder of precedence:\n\n - the act's `server_id`\n - the response header `accord-server-id`, if present\n - if the request is from a guild context, that guild\n\nThe `reason` string, when given, shows up in the guild's audit log.\n\n#### Response: text reply\n\nIn message create contexts (including commands), if a response has type\n`text/plain` is is read entirely as a UTF-8 string, and then treated as a\nsingle act with that string as content and no supplied `channel_id` (falling\nback to context or headers).\n\n#### Response: JSON presence\n\nThe `/discord/connecting` special endpoint is called _before_ the Accord\nconnects to Discord, and provides the opportunity to set the _presence_ of the\nbot. That is, its \"online / offline / dnd / etc\" status, whether it's marked as\nAFK, and what activity it's displaying, if any (the \"Playing some game...\"\nmessage under a user).\n\nIt's not yet possible to change the presence while connected.\n\n```typescript\n{\n  afk?: boolean,\n  status?: \"offline\" | \"online\" | \"dnd\" | \"idle\" | \"invisible\",\n  since?: number,\n  activity?: Activity\n}\n```\n\nThe `Activity` type can be any one of:\n\n```typescript\n{ playing: { name: string } }   // displays as `Playing {name}`\n{ streaming: { name: string } } // displays as `Streaming {name}`\n{ listening: { name: string } } // displays as `Listening to {name}`\n{ watching: { name: string } }  // displays as `Watching {name}`\n{ custom: { name: string } }    // may not be supported for bots yet\n```\n\n### Commands\n\nMessage create events can go to either their generic endpoints or, if they\nmatch and parse as a _command_, will go to the `/command/...` endpoint.\n\nThere are two (optional) environment variables controlling this:\n\n - `ACCORD_COMMAND_MATCH` does a simple regex test. If it matches, the message\n   is considered a command, otherwise not.\n\n - `ACCORD_COMMAND_PARSE` (if present) is then run on the message, and all\n   non-overlapping captures are collected and considered the \"command\" part of\n   the message.\n\nThe endpoint is then constructed to `/command/` followed by the parsed and\ncollected \"command\" parts as above joined by slashes.\n\nFor example, `!pick me` could be parsed to the endpoint `/command/pick/me`, or\nto `/command/pick`, or just to `/command/`, depending on what the parser regex\nis or if it's present at all.\n\nIf `ACCORD_COMMAND_MATCH` is not present, then nothing will go to `/command/...`.\n\nThe regex engine is the [regex](https://docs.rs/regex) crate with all defaults.\nYou can use this online tool to play/test regexes: https://rustexp.lpil.uk\n\nTo get started, try these:\n\n```\nACCORD_COMMAND_MATCH = ^!\\w+\nACCORD_COMMAND_PARSE = (?:^!|\\s+)(\\w+)\n```\n\n### Reverse interface\n\nAccord also has its own HTTP server listening, configured by the `ACCORD_BIND`\nvariable. This allows client-initiated functionality.\n\nAt the moment, only [Ghosts](#ghosts) are implemented.\n\n### Ghosts\n\nTo act on Discord spontaneously, there are currently two options:\n\n1. Make your own requests directly to Discord.\n2. Create and respond to ghost events on the reverse interface.\n\nGhost events are events your application generates and sends to Accord, which\nit then injects back into itself, as if they had come from Discord. Things\nproceed as normal from there. Ghosts are never sent to Discord, and only exist\nwithin the Accord instance they are sent to.\n\nThe primary purpose of ghosts is to initiate actions without external stimuli.\nFor example, a \"clock\" bot that posts a message every hour can summon, every\nhour, a ghost that sends the message `!clock`. Your server will then receive a\nrequest at `/command/clock`, answer appropriately, and Accord will post the\nreply up on Discord.\n\nGhosts can also be used to invoke a command from another command. For example,\ninvoking `!roll 1-9` could detect that the arguments are more appropriate for\nthe `!random` command, and send a ghost containing `!random 1-9`. That may be\nsimpler than the alternatives (or it may not, exercise your own judgement).\n\nTo summon a ghost, you make a request to `{ACCORD_BIND}/ghost/{ENDPOINT}` where\n`{ENDPOINT}` is the same endpoint [as in the forward\ninterface](#events-to-endpoint-table), containing the payload you would have\nreceived from that endpoint. The main difference is that you don't need to set\nany `accord-` headers (as there's no need to have them for routing). You also\ndon't need to set any payload field that is marked as optional.\n\nFor example, to send the `!clock` ghost as above, you would send a POST request\nto `/ghost/server/123/channel/456/message` with the JSON body:\n\n```json\n{\n  \"id\": 0,\n  \"server_id\": 123,\n  \"channel_id\": 456,\n  \"author\": {\n    \"server_id\": 123,\n    \"user\": {\n      \"id\": 0,\n      \"name\": \"a ghost\"\n    },\n  },\n  \"timestamp_created\": \"2020-01-02T03:04:05Z\",\n  \"content\": \"!clock\"\n}\n```\n\nThe server and channel IDs in the body will be preferred to the ones in the\nURL, but you should still set them correctly in the URL (for future\ncompatibility).\n\nCurrently only the following endpoints are implemented on the ghost interface:\n\n- server messages: `/ghosts/server/{guild-id}/channel/{channel-id}/message`\n- direct messages: `/ghosts/direct/channel/{channel-id}/message`\n\n### Test facility\n\nTo test an Accord server implementation, you could write a harness that queries\nyour server, but as there's multiple ways to respond to achieve the same thing\nin Discord, you would either start to replicate some Accord functionality just\nto coalesce these forms, or you would make tests too strict.\n\nAccord provides an `accord-tester` tool which works exactly the same as the main\nprogram, and takes the same variables (to the exception of `DISCORD_TOKEN`),\nexcept that instead of connecting to Discord, it only listens on the reverse\ninterface, and actions which would be taken on Discord are instead POSTed back\nto your server to `/test/act` in normalised form.\n\nDealing with the asynchronicity and lack of relationship between requests and\nreceived replies may be difficult; you can of course opt not to use this tool,\nor to use it for some integration tests only.\n\nThis tool defaults to `trace` level logging for accord, and you can opt in to\nprettier log messages by adding `pretty` to the `RUST_LOG` list, e.g.\n`RUST_LOG=pretty,info,accord=trace`.\n\n## Credits\n\n- The [logo](./logo.svg) is remixed from [the hands-helping solid Font Awesome icon](https://fontawesome.com/icons/hands-helping?style=solid), licensed under [CC-BY 4.0](https://fontawesome.com/license).\n\n## Background and Vision\n\nAccord is a Discord API client to power Discord API clients. Like bots. It is itself built on top of\nthe [Twilight] Discord API library. So, perhaps it should be called a middleware.\n\n[Twilight]: https://github.com/twilight-rs/twilight\n\nAccord is about translating a specialised interface (Discord's API) to a very common interface (HTTP\ncalls to a server), and back.\n\nOne thing I find when writing Discord bots is that a lot of logic that is already reliably\nimplemented by other software a lot older than Discord itself has to regularly be reimplemented for\na bot's particular usecase... and I'd rather be writing business logic.\n\nAnother is that invariably whenever I start a Discord bot project I end up wanting to write some\nparts of it in a different language or using a different stack. If I had Accord, I could.\n\nSo, in Accord, a typical interaction with a bot would go like this:\n\n1. Someone invokes the bot, e.g. by saying `!roll d20`\n2. Discord sends Accord the message via WebSocket\n3. Accord makes a POST request to `http://localhost:1234/server/123/channel/456/message` with the\n   message contents as the body, plus various bits of metadata in the headers\n4. Your \"bot\" which is really a server listening on port 1234 accepts that request, processes it\n   (rolls a d20) and returns the answer in the response body with code 200\n5. Accord reads the response, sees it means to reply, adds in the channel and server/guild\n   information if those weren't provided in the response headers\n6. Accord posts a message to Discord containing the reply.\n\nYou don't need to have your bot listen on port 1234 itself. In fact, that is not recommended. What\nyou should do instead is run it behind nginx. Why? Let's answer with another few scenarios:\n\n- What if the answer to a command is always the same?\n\n  Instead of having an active server process and answer the same thing every time, write an nginx\n  rule to match that request and reply with the contents of a static file on disk.\n\n- What if the answer changes infrequently?\n\n  Add a cache. This is built-in to nginx in a few different ways, or use Varnish or something.\n\n- What if the answer is expensive, and/or you don't want it abused?\n\n  Add rate limiting. This is built-in to nginx.\n\n- What if you want to scale out the amount of backends?\n\n  Scale horizontally, use nginx's round-robin upstream support.\n\n- What if you want to partially scale *in*, for example because you serve lots of guilds and need to\n  shard for your expensive endpoints, but your cheap endpoints are perfectly capable of handling the\n  load?\n\n  Point your sharded Accords to their own nginxes, and forward cheap requests to one backend server.\n\nThere are many more fairly common scenarios that, in usual Discord bots, would require a lot of\nengineering, but with the Accord approach, **are already solved**.\n\nOkay, but, that may work well for query-reply bots, but your bot needs to reply more than once, or\nneeds to post spontaneously, for example in response to an external event.\n\nThere are four approaches with Accord.\n\n1. Do the call yourself. Have a Discord client in your app that calls out. Accord doesn't (and\n   cannot) stop you from doing this.\n\n2. Use the reverse interface. Accord exposes a server of its own, and you can make requests to that\n   server to use Accord's Discord connection to make requests. Accord adds authentication to Discord\n   on top, so you don't need to handle credentials in two places.\n\n3. Summon a ghost. You can make a special call via the reverse interface mentioned above that will\n   cause Accord to fire a request in the usual way, as if it was reacting to a message or other\n   Discord action, but actually that message or action does not exist. In that way you can\n   implement code all the same, and take advantage of the existing layering (cache etc).\n\n4. In the special case of needing to answer multiple times in response to an event, you can respond\n   using chunked output, keep the output stream alive with null bytes sent at 30–60s intervals, and\n   send through multiple payloads separated by at least two null bytes. The payloads will be sent\n   as soon as each one is received.\n\nWhat if you need to stream some audio to a voice channel?\n\n- You can stream audio, in whatever format Accord supports (and it will transcode on the fly if it's\n  not something Discord supports), as the response.\n\n- You can reply with a 302 redirect to a static audio file, and Accord will do the same (but it\n  might be a little more clever in regards to buffering if it detects it can do range requests).\n  You can even redirect to an external resource (not recommended, for security and performance\n  reasons... but you can do it).\n\n### Beyond Discord\n\nThis... is only the beginning.\n\nBecause Discord is one thing, but what if you had this same kind of gateway for Twitter? Matrix?\nZulip? IRC? Slack? Email? etc\n\nAll these have anywhere from slightly to majorly different models in how they operate, but you still\nhave a core mechanic of posting messages and expecting answers. You'll probably have subtleties and\nadaptations, but what if you could reuse vast swathes of functionality by _rewriting some routes_?\n\nThe first example above, a bot that rolls a die, could be the exact same backend program served for\nSlack and for Discord. At the same time, and in parallel, you could have a Discord-specific voice\nendpoint, and a Slack-specific poll endpoint.\n\n### Isn't this really inefficient?\n\nYeah, kinda. Instead of a bot that interacts directly with Discord, you have at least two additional\nlayers. All that adds is a few tens of milliseconds. What you _gain_ is likely worth it. By a lot.\n","funding_links":[],"categories":["Libraries"],"sub_categories":["Rust"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpasscod%2Faccord","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpasscod%2Faccord","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpasscod%2Faccord/lists"}