You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
This repository was archived by the owner on Jan 2, 2026. It is now read-only.
Want to contribute to Chorus by implementing a missing API route, but don't know where and how to start? This forum post should cover the basics about what to write and where. I will be using the "Modify Guild" route as an example.
File Structure
When cloning chorus and looking inside the repository folder, you should see something like this:
Here's a breakdown of all the folders and files we'll be touching in this example:
Folder/Subfolder/File
Explanation
/src/api/
Holds all the API route implementations. Features subfolders like 'auth', 'channels' to organize functionality a bit better.
/src/api/guilds/guilds.rs
The file we will be placing our new API route in.
/src/types/entities/
Houses all the different entity structs, which describe, what a Guild, a Channel or a User look like.
/src/types/schema/
Describes all schemas. Schemas contain all the input fields the server wants from you when performing actions like registrating, logging in, modifying a user, etc.
/src/instance.rs
Contains the Instance and ChorusUser structs, which represent the Instance and the user whose account is being puppeteered by this library respectively.
/src/ratelimiter.rs
Contains all the logic for sending requests to the API, as well as functions for processing them.
The signature of our new function
Since the new function is supposed to modify a Guild, we will be placing it inside /src/api/guilds/guilds.rs's impl Guild {...} block. The function signature will look like this:
The function is supposed to be part of the public chorus API, therefore requiring the visibility modifier pub. Network requests are sent to the server and awaited in an asynchronous manner, calling for the async fn keywords.
From looking at this, we can already tell a few things:
{guild.id} is the ID of the Guild we want to modify, so we'll have to pass it in the function signature: guild_id: Snowflake (since Discord IDs are always Snowflakes.)
This route takes an optional Audit Log Reason, which we'll represent with audit_log_reason: Option<String>
This route requires MFA. We'll touch on how to handle this later.
This route requires user authorization - we can tell this because a) routes that don't require it have a specific tag (Unauthenticated) and b) because the docs talk about a permission the user needs. So, in our function signature, we'll reflect this with user: &mut ChorusUser (we could also decide to implement this function in impl ChorusUser { ... } - in such a case, we'd add &mut self as the first argument.)
If we scroll a bit further down the page, we'll see this:
This is the JSON body we will be sending to the server. We can see that it requires a lot of fields, which is why in the next step, we will be creating a new schema for this route. The schema will be placed inside /src/types/schema/guilds.rs and will look like this:
Hint: The documentation page sometimes (in this case, always) specifies a "?" after a field name. This means that the field is optional and can be omitted. This is why all fields in the schema are wrapped in Option<>s.
The return type
Now that we have covered all function arguments, let's look at the function return type!
The Discord Userdoccers state:
...Returns the updated guild object on success.
In Chorus/the Discord API, we generally only encounter two different return types:
ChorusResult<..., E>
ChorusResult<()>: This is the return type for all routes that don't return anything. The () is the Rust equivalent of void in C++. The ChorusResult is a type alias for Result<(), ChorusError>, which is a Result that either returns Ok(()) (so - nothing) or Err(ChorusError), in case something went wrong.
ChorusResult<T>, where T is an entity: This is the return type for all routes that return something. ChorusResult<...> means the same thing as it does in the previous example, but instead of () we have a generic type T, which is an entity. In this case, the function will return Ok(T) if everything went well, or Err(ChorusError) if something went wrong.
In the case of our modify function, we will be returning ChorusResult<Guild>, since we expect the server to return the updated Guild object.
And there we have it! The function signature is complete. Now, let's take a look at the function body.
The function body
Let's take a look at, and then dissect the function body:
The first thing we do is create a new ChorusRequest. The ChorusRequest is a struct that contains all the information needed to send a request to the server. It contains the request itself, as well as the LimitType of the request. The LimitType is an enum that describes the type of request we are sending and is important to tell Chorus' ratelimiter what sort of Request it is dealing with. In this case, we are sending a request to modify a Guild, so we will be using LimitType::Guild(guild_id). The guild_id is the ID of the Guild we are modifying.
The ChorusRequest takes two arguments:
The request itself. This is a reqwest::RequestBuilder.
The LimitType of the request.
Making the RequestBuilder
If reqwest::Client is imported via a use statement, creating a new RequestBuilder is as easy as calling Client::new().<method>(<url>). In this case, we are using the patch method, since we are sending a PATCH request to the server (Discord Userdoccers tell us this information). The URL is the API URL of the Instance we are sending the request to, plus the endpoint we are sending the request to. In this case, we are sending a PATCH request to https://api.polyphony.rocks/guilds/<guild_id>, so we are using format!("{}/guilds/{}", user.belongs_to.borrow().urls.api, guild_id) to create the URL.
Hint: I recommend reading the reqwest::Client documentation to learn more about the different methods and how to use them.
Adding the JSON body
Let's add the JSON body to the request, so that the Server can actually know what our user wants to modify about the guild. This is done by calling .json(&schema) on the RequestBuilder. (Some older routes will manually call serde on the schema, but reqwest can save us a bit of work.) This serializes the schema into a JSON string, which is then added to the request. Note that this is only possible, if you annotated the schema (in this case: GuildModifySchema) with #[derive(Serialize)].
Adding the LimitType
The last thing we do in the request's constructor is add the LimitType to the ChorusRequest. Just type LimitType:: into your IDE and let the autocomplete suggestions handle the rest. Note, that you will have to add the guild_id as an argument to the LimitType::Guild variant.
Adding MFA
Since we saw earlier that this request requires MFA, we'll want to send it if we have an active token. We do this with .with_maybe_mfa(&user.mfa_token) - this method will add the user's override token if they have a valid one. If they don't, we can't send it anyway and the server will kindly inform us how to get it with an error.
Adding Audit Log Reason
Similar to the MFA token, we'll tell the ChorusRequest to send the Audit Log Reason header if the user has specified one: .with_maybe_audit_log_reason(audit_log_reason)
Adding User headers
The last thing we need to do is add the user's information to the request, so the server knows who we are. (This is the Authorization header and certain telemetry headers which make our request seem more "real".) We simply do this with .with_headers_for(user); and let chorus' request framework handle the rest.
Great! We now have the ChorusRequest and can send it to the server. But how do we do that?
Sending the request and handling the response
We have previously talked about the two different return types of Chorus/Discord API endpoints. Luckily for you, Chorus has functions that handles both of them for you: ChorusRequest::send_and_deserialize_response::<T>(user: &mut ChorusUser), if you expect a response of type T, and ChorusRequest::send_and_handle_as_result(user: &mut ChorusUser) if you expect no meaningful answer from the server (e.g. a 204 No Content response).
In this case, we will be using the send_and_deserialize_response function, since we expect the server to return the updated Guild object. The function takes one argument: A mutable reference to the ChorusUser struct. This is needed for all the fancy logic which is involved in sending requests to the server. The send_and_deserialize_response function needs to know the type of the response, so it can deserialize the response into the correct type. In this case, we are expecting a Guild object, so we will be using send_and_deserialize_response::<Guild>(user).
Hint: Don't forget to .await the function call! :P
The send_and_deserialize_response function returns a ChorusResult<Guild>, which is the same as Result<Guild, ChorusError>. This means that we can use the ? operator to return the Guild object if everything went well, or return the ChorusError if something went wrong. This is done by just adding a ? after the function call.
The last thing we do is wrap the Guild object in an Ok() and return it. This is done by calling Ok(response).
A note on anonymous requests
A small portion of requests aren't done in the context of a user account (ChorusUser) - a nice example is fetching an Instance's public configuration. In chorus we call these "anonymous requests" and usually add them in an impl Instance { ... } block (as opposed to impl ChorusUser { ... }). These requests are internally handled in a special way, but all you need to know for now is that Instance replaces ChorusUser in most cases. The methods to handle requests are also slightly different - ChorusRequest::send_and_deserialize_response and ChorusRequest::send_and_handle_as_result are replaced by ChorusRequest::send_anonymous_and_deserialize_response and ChorusRequest::send_anonymous_and_handle_as_result. The with_headers_for(user) method is simply not needed.
Finishing touches
These final few things aren't required for a working route and accepted contribution, but the maintainers will really appreciate it if you take the time to add them.
Documentation
No production code is truly complete without documentation. You don't need to spend hours researching the specific nuances of one obscure API route, but it helps users to know what your method does, any special things about using it, and where they can learn more. You can usually find everything you need on the docs page you referenced for the implementation.
Our documentation usually looks like so:
/// [A brief description of the type / method, what it does]////// [Permissions the user needs, special cases, gateway events the method fires, ...]////// # Notes/// [Special things to note, such as that it requires MFA, that it's not supported on or is specific to one certain server software, that it's an alias of another route ...]////// # Reference/// See [A link to the docs page, surrounded by < and > to make it clickable]
For example, consider the docs for the route we added above:
/// Modify a guild's settings.////// Requires the [MANAGE_GUILD](crate::types::PermissionFlags::MANAGE_GUILD) permission.////// Returns the updated guild.////// Fires a [GuildUpdate](crate::types::GuildUpdate) gateway event.////// # Notes/// This route requires MFA.////// # Reference/// See <https://discord-userdoccers.vercel.app/resources/guild#modify-guild>pubasyncfnmodify(...){ ...}
We should also take care to document the schema we added:
/// Represents the schema used to modify a guild.////// # Reference/// See <https://docs.discord.sex/resources/guild#modify-guild>pubstructGuildModifySchema{ ... }
Nothing special, but we at least tell users and other developers where we got our information from.
Yay
Congratulations! You have successfully implemented a new API route! You should now take a look at how to test it, and then submit a pull request to the Chorus repository. If you have any questions, feel free to ask them in the #chorus-discussion channel on the Polyphony Discord server. We will be happy to help you out. :)
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Want to contribute to Chorus by implementing a missing API route, but don't know where and how to start? This forum post should cover the basics about what to write and where. I will be using the "Modify Guild" route as an example.
File Structure
When cloning chorus and looking inside the repository folder, you should see something like this:
Here's a breakdown of all the folders and files we'll be touching in this example:
/src/api//src/api/guilds/guilds.rs/src/types/entities/Guild, aChannelor aUserlook like./src/types/schema//src/instance.rsInstanceandChorusUserstructs, which represent the Instance and the user whose account is being puppeteered by this library respectively./src/ratelimiter.rsThe signature of our new function
Since the new function is supposed to modify a Guild, we will be placing it inside
/src/api/guilds/guilds.rs'simpl Guild {...}block. The function signature will look like this:Let's break this down bit by bit.
The function is supposed to be part of the public chorus API, therefore requiring the visibility modifier
pub. Network requests are sent to the server and awaited in an asynchronous manner, calling for theasync fnkeywords.I personally like using Discord Userdoccers to look at Discord API Documentation, so let's take a look at what it has got to say about the Guild::modify route:
From looking at this, we can already tell a few things:
{guild.id}is the ID of the Guild we want to modify, so we'll have to pass it in the function signature:guild_id: Snowflake(since Discord IDs are alwaysSnowflakes.)audit_log_reason: Option<String>user: &mut ChorusUser(we could also decide to implement this function inimpl ChorusUser { ... }- in such a case, we'd add&mut selfas the first argument.)If we scroll a bit further down the page, we'll see this:

This is the JSON body we will be sending to the server. We can see that it requires a lot of fields, which is why in the next step, we will be creating a new schema for this route. The schema will be placed inside
/src/types/schema/guilds.rsand will look like this:A hint on optional parameters
Hint: The documentation page sometimes (in this case, always) specifies a "?" after a field name. This means that the field is optional and can be omitted. This is why all fields in the schema are wrapped in
Option<>s.The return type
Now that we have covered all function arguments, let's look at the function return type!
The Discord Userdoccers state:
In Chorus/the Discord API, we generally only encounter two different return types:
ChorusResult<..., E>
ChorusResult<()>: This is the return type for all routes that don't return anything. The()is the Rust equivalent ofvoidin C++. TheChorusResultis a type alias forResult<(), ChorusError>, which is aResultthat either returnsOk(())(so - nothing) orErr(ChorusError), in case something went wrong.ChorusResult<T>, where T is an entity: This is the return type for all routes that return something.ChorusResult<...>means the same thing as it does in the previous example, but instead of()we have a generic typeT, which is an entity. In this case, the function will returnOk(T)if everything went well, orErr(ChorusError)if something went wrong.In the case of our
modifyfunction, we will be returningChorusResult<Guild>, since we expect the server to return the updatedGuildobject.And there we have it! The function signature is complete. Now, let's take a look at the function body.
The function body
Let's take a look at, and then dissect the function body:
ChorusRequest
The first thing we do is create a new
ChorusRequest. TheChorusRequestis a struct that contains all the information needed to send a request to the server. It contains the request itself, as well as theLimitTypeof the request. TheLimitTypeis an enum that describes the type of request we are sending and is important to tell Chorus' ratelimiter what sort of Request it is dealing with. In this case, we are sending a request to modify a Guild, so we will be usingLimitType::Guild(guild_id). Theguild_idis the ID of the Guild we are modifying.The
ChorusRequesttakes two arguments:reqwest::RequestBuilder.LimitTypeof the request.Making the RequestBuilder
If
reqwest::Clientis imported via ausestatement, creating a new RequestBuilder is as easy as callingClient::new().<method>(<url>). In this case, we are using thepatchmethod, since we are sending a PATCH request to the server (Discord Userdoccers tell us this information). The URL is the API URL of the Instance we are sending the request to, plus the endpoint we are sending the request to. In this case, we are sending a PATCH request tohttps://api.polyphony.rocks/guilds/<guild_id>, so we are usingformat!("{}/guilds/{}", user.belongs_to.borrow().urls.api, guild_id)to create the URL.Hint: I recommend reading the reqwest::Client documentation to learn more about the different methods and how to use them.
Adding the JSON body
Let's add the JSON body to the request, so that the Server can actually know what our user wants to modify about the guild. This is done by calling
.json(&schema)on the RequestBuilder. (Some older routes will manually call serde on the schema, butreqwestcan save us a bit of work.) This serializes the schema into a JSON string, which is then added to the request. Note that this is only possible, if you annotated the schema (in this case:GuildModifySchema) with#[derive(Serialize)].Adding the LimitType
The last thing we do in the request's constructor is add the
LimitTypeto theChorusRequest. Just typeLimitType::into your IDE and let the autocomplete suggestions handle the rest. Note, that you will have to add theguild_idas an argument to theLimitType::Guildvariant.Adding MFA
Since we saw earlier that this request requires MFA, we'll want to send it if we have an active token. We do this with
.with_maybe_mfa(&user.mfa_token)- this method will add the user's override token if they have a valid one. If they don't, we can't send it anyway and the server will kindly inform us how to get it with an error.Adding Audit Log Reason
Similar to the MFA token, we'll tell the
ChorusRequestto send the Audit Log Reason header if the user has specified one:.with_maybe_audit_log_reason(audit_log_reason)Adding User headers
The last thing we need to do is add the user's information to the request, so the server knows who we are. (This is the
Authorizationheader and certain telemetry headers which make our request seem more "real".) We simply do this with.with_headers_for(user);and letchorus' request framework handle the rest.Great! We now have the
ChorusRequestand can send it to the server. But how do we do that?Sending the request and handling the response
We have previously talked about the two different return types of Chorus/Discord API endpoints. Luckily for you, Chorus has functions that handles both of them for you:
ChorusRequest::send_and_deserialize_response::<T>(user: &mut ChorusUser), if you expect a response of type T, andChorusRequest::send_and_handle_as_result(user: &mut ChorusUser)if you expect no meaningful answer from the server (e.g. a 204 No Content response).In this case, we will be using the
send_and_deserialize_responsefunction, since we expect the server to return the updatedGuildobject. The function takes one argument: A mutable reference to theChorusUserstruct. This is needed for all the fancy logic which is involved in sending requests to the server. Thesend_and_deserialize_responsefunction needs to know the type of the response, so it can deserialize the response into the correct type. In this case, we are expecting aGuildobject, so we will be usingsend_and_deserialize_response::<Guild>(user).Hint: Don't forget to .await the function call! :P
The
send_and_deserialize_responsefunction returns aChorusResult<Guild>, which is the same asResult<Guild, ChorusError>. This means that we can use the?operator to return theGuildobject if everything went well, or return theChorusErrorif something went wrong. This is done by just adding a?after the function call.The last thing we do is wrap the
Guildobject in anOk()and return it. This is done by callingOk(response).A note on anonymous requests
A small portion of requests aren't done in the context of a user account (
ChorusUser) - a nice example is fetching anInstance's public configuration. Inchoruswe call these "anonymous requests" and usually add them in animpl Instance { ... }block (as opposed toimpl ChorusUser { ... }). These requests are internally handled in a special way, but all you need to know for now is thatInstancereplacesChorusUserin most cases. The methods to handle requests are also slightly different -ChorusRequest::send_and_deserialize_responseandChorusRequest::send_and_handle_as_resultare replaced byChorusRequest::send_anonymous_and_deserialize_responseandChorusRequest::send_anonymous_and_handle_as_result. Thewith_headers_for(user)method is simply not needed.Finishing touches
These final few things aren't required for a working route and accepted contribution, but the maintainers will really appreciate it if you take the time to add them.
Documentation
No production code is truly complete without documentation. You don't need to spend hours researching the specific nuances of one obscure API route, but it helps users to know what your method does, any special things about using it, and where they can learn more. You can usually find everything you need on the docs page you referenced for the implementation.
Our documentation usually looks like so:
For example, consider the docs for the route we added above:
We should also take care to document the schema we added:
Nothing special, but we at least tell users and other developers where we got our information from.
Yay
Congratulations! You have successfully implemented a new API route! You should now take a look at how to test it, and then submit a pull request to the Chorus repository. If you have any questions, feel free to ask them in the #chorus-discussion channel on the Polyphony Discord server. We will be happy to help you out. :)
Beta Was this translation helpful? Give feedback.
All reactions