parent
5c93676b32
commit
ad957f15be
@ -0,0 +1 @@ |
||||
node_modules |
@ -0,0 +1,35 @@ |
||||
{ |
||||
"env": { |
||||
"es6": true, |
||||
"node": true |
||||
}, |
||||
"parserOptions": { |
||||
"ecmaVersion": 2020 |
||||
}, |
||||
"extends": "eslint:recommended", |
||||
"rules": { |
||||
"semi": "error", |
||||
"no-var": "error", |
||||
"prefer-const": "error", |
||||
"keyword-spacing": "error", |
||||
"eqeqeq": "error", |
||||
"eol-last": "error", |
||||
"brace-style": ["error", "stroustrup"], |
||||
"comma-dangle": ["error", "always-multiline"], |
||||
"object-curly-spacing": ["error", "always"], |
||||
|
||||
"quotes": ["error", "double", { |
||||
"allowTemplateLiterals": true |
||||
}], |
||||
|
||||
"indent": ["error", "tab", { |
||||
"SwitchCase": 1, |
||||
"flatTernaryExpressions": true |
||||
}], |
||||
|
||||
"object-curly-newline": ["error", { |
||||
"ExportDeclaration": "never", |
||||
"ImportDeclaration": "always" |
||||
}] |
||||
} |
||||
} |
@ -0,0 +1,21 @@ |
||||
--- |
||||
name: Can't get it to work |
||||
about: Use this template if you are having issues with the library (all issues opened |
||||
for issues with the library that don't follow this template will be ignored and |
||||
closed). |
||||
title: '' |
||||
labels: '' |
||||
assignees: reboxer |
||||
|
||||
--- |
||||
|
||||
**Describe the error** |
||||
A clear and concise description of what the error is. |
||||
|
||||
**NodeJS version** |
||||
*Your NodeJS version* |
||||
|
||||
**Relevant code** |
||||
```js |
||||
// Put here the code you are using that is not working |
||||
``` |
@ -0,0 +1,18 @@ |
||||
name: Run ESLint |
||||
|
||||
on: |
||||
push: |
||||
branches: [ master ] |
||||
pull_request: |
||||
branches: [ master ] |
||||
|
||||
jobs: |
||||
lint: |
||||
name: Run ESlint |
||||
|
||||
runs-on: ubuntu-latest |
||||
|
||||
steps: |
||||
- uses: actions/checkout@v2 |
||||
- run: yarn |
||||
- run: yarn run lint |
@ -0,0 +1,21 @@ |
||||
MIT License |
||||
|
||||
Copyright (c) 2019 reboxer |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy |
||||
of this software and associated documentation files (the "Software"), to deal |
||||
in the Software without restriction, including without limitation the rights |
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell |
||||
copies of the Software, and to permit persons to whom the Software is |
||||
furnished to do so, subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE |
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, |
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE |
||||
SOFTWARE. |
@ -0,0 +1,358 @@ |
||||
# discord-oauth2 [![NPM version](https://img.shields.io/npm/v/discord-oauth2.svg?style=flat-square)](https://www.npmjs.com/package/discord-oauth2) |
||||
|
||||
A really simple to use module to use discord's OAuth2 API. |
||||
|
||||
Please check out discord's OAuth2 documentation: https://discord.com/developers/docs/topics/oauth2 |
||||
|
||||
### Installing |
||||
|
||||
```bash |
||||
npm install discord-oauth2 |
||||
``` |
||||
|
||||
# Class constructor |
||||
|
||||
One parameter is passed to the class constructor: |
||||
|
||||
### `Options` |
||||
|
||||
Since the module uses a modified version of [Eris](https://github.com/abalabahaha/eris) request handler, it takes the same options, all of them default to the default Eris Client options if no options are passed. |
||||
|
||||
Request handler options: |
||||
``` |
||||
requestTimeout: A number of milliseconds before requests are considered timed out. |
||||
|
||||
latencyThreshold: The average request latency at which the RequestHandler will start emitting latency errors. |
||||
|
||||
ratelimiterOffset: A number of milliseconds to offset the ratelimit timing calculations by. |
||||
|
||||
``` |
||||
|
||||
Others, you can pass these options to the class constructor so you don't have to pass them each time you call a function: |
||||
``` |
||||
version: The Discord API version to use. Defaults to "v7". |
||||
|
||||
clientId: Your application's client id. |
||||
|
||||
clientSecret: Your application's client secret. |
||||
|
||||
redirectUri: Your URL redirect uri. |
||||
|
||||
credentials: Base64 encoding of the UTF-8 encoded credentials string of your application, you can pass this in the constructor to not pass it every time you want to use the revokeToken() method. |
||||
``` |
||||
|
||||
# Events |
||||
|
||||
In the Eris Library, client extends the `events` modules and the client is passed to the RequestHandler so it's able to emit events, this modified RequestHandler extends `events` so it can emit the same events. |
||||
|
||||
There are only two events, `debug` and `warn`. |
||||
|
||||
# Methods |
||||
|
||||
### `tokenRequest()` |
||||
|
||||
Only takes an object with the following properties: |
||||
|
||||
`clientId`: Your application's client id. Can be omitted if provided on the client constructor. |
||||
|
||||
`clientSecret`: Your application's client secret. Can be omitted if provided on the client constructor. |
||||
|
||||
`scope`: The scopes requested in your authorization url, can be either a space-delimited string of scopes, or an array of strings containing scopes. |
||||
|
||||
`redirectUri`: Your URL redirect uri. Can be omitted if provided on the client constructor. |
||||
|
||||
`grantType`: The grant type to set for the request, either authorization_code or refresh_token. |
||||
|
||||
`code`: The code from the querystring (grantType `authorization_code` only). |
||||
|
||||
`refreshToken`: The user's refresh token (grantType `refresh_token` only). |
||||
|
||||
|
||||
Returns a promise which resolves in an object with the access token. |
||||
|
||||
Please refer to discord's OAuth2 [documentation](https://discord.com/developers/docs/topics/oauth2#authorization-code-grant-access-token-exchange-example) for the parameters needed. |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2(); |
||||
|
||||
oauth.tokenRequest({ |
||||
clientId: "332269999912132097", |
||||
clientSecret: "937it3ow87i4ery69876wqire", |
||||
|
||||
code: "query code", |
||||
scope: "identify guilds", |
||||
grantType: "authorization_code", |
||||
|
||||
redirectUri: "http://localhost/callback", |
||||
}).then(console.log) |
||||
``` |
||||
|
||||
Using class constructor options, array of scopes and grantType refresh_token: |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2({ |
||||
clientId: "332269999912132097", |
||||
clientSecret: "937it3ow87i4ery69876wqire", |
||||
redirectUri: "http://localhost/callback", |
||||
}); |
||||
|
||||
oauth.tokenRequest({ |
||||
// clientId, clientSecret and redirectUri are omitted, as they were already set on the class constructor |
||||
refreshToken: "D43f5y0ahjqew82jZ4NViEr2YafMKhue", |
||||
grantType: "refresh_token", |
||||
scope: ["identify", "guilds"], |
||||
}); |
||||
|
||||
// On successful request both requesting and refreshing an access token return the same object |
||||
/* |
||||
{ |
||||
"access_token": "6qrZcUqja7812RVdnEKjpzOL4CvHBFG", |
||||
"token_type": "Bearer", |
||||
"expires_in": 604800, |
||||
"refresh_token": "D43f5y0ahjqew82jZ4NViEr2YafMKhue", |
||||
"scope": "identify guilds" |
||||
} |
||||
*/ |
||||
``` |
||||
|
||||
### `revokeToken()` |
||||
|
||||
Takes two parameters, the first one is the access_token from the user, the second is a Base64 encoding of the UTF-8 encoded credentials string of your application. |
||||
|
||||
Returns a promise which resolves in an empty object if successful. |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2(); |
||||
|
||||
const clientID = "332269999912132097"; |
||||
const client_secret = "937it3ow87i4ery69876wqire"; |
||||
const access_token = "6qrZcUqja7812RVdnEKjpzOL4CvHBFG"; |
||||
|
||||
// You must encode your client ID along with your client secret including the colon in between |
||||
const credentials = Buffer.from(`${clientID}:${client_secret}`).toString("base64"); // MzMyMjY5OTk5OTEyMTMyMDk3OjkzN2l0M293ODdpNGVyeTY5ODc2d3FpcmU= |
||||
|
||||
oauth.revokeToken(access_token, credentials).then(console.log); // {} |
||||
``` |
||||
|
||||
### `getUser()` |
||||
|
||||
Only takes one parameter which is the user's access token. |
||||
|
||||
Returns the [user](https://discord.com/developers/docs/resources/user#user-object) object of the requester's account, this requires the `identify` scope, which will return the object without an email, and optionally the `email` scope, which returns the object with an email. |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2(); |
||||
|
||||
const access_token = "6qrZcUqja7812RVdnEKjpzOL4CvHBFG"; |
||||
|
||||
oauth.getUser(access_token).then(console.log); |
||||
/* |
||||
{ |
||||
username: '1337 Krew', |
||||
locale: 'en-US', |
||||
mfa_enabled: true, |
||||
flags: 128, |
||||
avatar: '8342729096ea3675442027381ff50dfe', |
||||
discriminator: '4421', |
||||
id: '80351110224678912' |
||||
} |
||||
*/ |
||||
``` |
||||
|
||||
### `getUserGuilds()` |
||||
|
||||
Only takes one parameter which is the user's access token. |
||||
|
||||
Returns a list of partial [guild](https://discord.com/developers/docs/resources/guild#guild-object) objects the current user is a member of. Requires the `guilds` scope. |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2(); |
||||
|
||||
const access_token = "6qrZcUqja7812RVdnEKjpzOL4CvHBFG"; |
||||
|
||||
oauth.getUserGuilds(access_token).then(console.log); |
||||
/* |
||||
{ |
||||
"id": "80351110224678912", |
||||
"name": "1337 Krew", |
||||
"icon": "8342729096ea3675442027381ff50dfe", |
||||
"owner": true, |
||||
"permissions": 36953089, |
||||
"permissions_new": "36953089" |
||||
} |
||||
*/ |
||||
``` |
||||
|
||||
### `getUserConnections()` |
||||
|
||||
Only takes one parameter which is the user's access token. |
||||
|
||||
Returns a list of [connection](https://discord.com/developers/docs/resources/user#connection-object) objects. Requires the `connections` OAuth2 scope. |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2(); |
||||
|
||||
const access_token = "6qrZcUqja7812RVdnEKjpzOL4CvHBFG"; |
||||
|
||||
oauth.getUserConnections(access_token).then(console.log); |
||||
/* |
||||
[ { verified: true, |
||||
name: 'epicusername', |
||||
show_activity: true, |
||||
friend_sync: false, |
||||
type: 'twitch', |
||||
id: '31244565', |
||||
visibility: 1 } ] |
||||
*/ |
||||
``` |
||||
|
||||
### `addMember()` |
||||
|
||||
Force join a user to a guild (server). |
||||
|
||||
Takes an object with the following properties: |
||||
|
||||
`accessToken`: The user access token. |
||||
|
||||
`botToken`: The token of the bot used to authenticate. |
||||
|
||||
`guildId`: The ID of the guild to join. |
||||
|
||||
`userId`: The ID of the user to be added to the guild. |
||||
|
||||
Optional properties (the above ones are required): |
||||
|
||||
`nickname`: Value to set users nickname to. |
||||
|
||||
`roles`: Array of role ids the member is assigned. |
||||
|
||||
`mute`: Whether the user is muted in voice channels. |
||||
|
||||
`deaf`: Whether the user is deafened in voice channels. |
||||
|
||||
Returns a member object if the user wasn't part of the guild, else, returns an empty string (length 0). |
||||
|
||||
```js |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2(); |
||||
|
||||
oauth.addMember({ |
||||
accessToken: "2qRZcUqUa9816RVnnEKRpzOL2CvHBgF", |
||||
botToken: "NDgyMjM4ODQzNDI1MjU5NTIz.XK93JQ.bnLsc71_DGum-Qnymb4T5F6kGY8", |
||||
guildId: "216488324594438692", |
||||
userId: "80351110224678912", |
||||
|
||||
nickname: "george michael", |
||||
roles: ["624615851966070786"], |
||||
mute: true, |
||||
deaf: true, |
||||
}).then(console.log); // Member object or empty string |
||||
|
||||
/* |
||||
{ |
||||
nick: 'george michael', |
||||
user: { |
||||
username: 'some username', |
||||
discriminator: '0001', |
||||
id: '421610529323943943', |
||||
avatar: null |
||||
}, |
||||
roles: [ '324615841966570766' ], |
||||
premium_since: null, |
||||
deaf: true, |
||||
mute: true, |
||||
joined_at: '2019-09-20T14:44:12.603123+00:00' |
||||
} |
||||
*/ |
||||
``` |
||||
|
||||
### `generateAuthUrl` |
||||
|
||||
Dynamically generate an OAuth2 URL. |
||||
|
||||
Takes an object with the following properties: |
||||
|
||||
`clientId`: Your application's client id. Can be omitted if provided on the client constructor. |
||||
|
||||
`prompt`: Controls how existing authorizations are handled, either consent or none (for passthrough scopes authorization is always required). |
||||
|
||||
`scope`: The scopes requested in your authorization url, can be either a space-delimited string of scopes, or an array of strings containing scopes. |
||||
|
||||
`redirectUri`: Your URL redirect uri. Can be omitted if provided on the client constructor. |
||||
|
||||
`responseType`: The response type, either code or token (token is for client-side web applications only). Defaults to code. |
||||
|
||||
`state`: A unique cryptographically secure string (https://discord.com/developers/docs/topics/oauth2#state-and-security). |
||||
|
||||
`permissions`: The permissions number for the bot invite (only with bot scope) (https://discord.com/developers/docs/topics/permissions). |
||||
|
||||
`guildId`: The guild id to pre-fill the bot invite (only with bot scope). |
||||
|
||||
`disableGuildSelect`: Disallows the user from changing the guild for the bot invite, either true or false (only with bot scope). |
||||
|
||||
```js |
||||
const crypto = require('crypto') |
||||
const DiscordOauth2 = require("discord-oauth2"); |
||||
const oauth = new DiscordOauth2({ |
||||
clientId: "332269999912132097", |
||||
clientSecret: "937it3ow87i4ery69876wqire", |
||||
redirectUri: "http://localhost/callback", |
||||
}); |
||||
|
||||
const url = oauth.generateAuthUrl({ |
||||
scope: ["identify", "guilds"], |
||||
state: crypto.randomBytes(16).toString("hex"), // Be aware that randomBytes is sync if no callback is provided |
||||
}); |
||||
|
||||
console.log(url); |
||||
// https://discord.com/api/oauth2/authorize?client_id=332269999912132097&redirect_uri=http%3A%2F%2Flocalhost%2Fcallback&response_type=code&scope=identify%20guilds&state=132054f372bfca771de3dfe54aaacece |
||||
|
||||
``` |
||||
|
||||
# Debugging |
||||
|
||||
By default when you log an error to the console, it will look something like this `DiscordHTTPError: 400 Bad Request on POST /api/v7/oauth2/token` followed by a very long stack trace what most of the times won't be useful (if you already know where the function is called). |
||||
|
||||
To easily debug any issues you are having, you can access the following properties of the error object thrown: |
||||
|
||||
`req`: The HTTP request sent to discord. |
||||
|
||||
`res`: The HTTP response sent from discord to our request. |
||||
|
||||
`code`: If the error is a `DiscordHTTPError`, it will be the HTTP status code of the response (same as `res.statusCode`).<br /> |
||||
If the error is a `DiscordRESTError`, it will be a [Discord API JSON error code](https://discord.com/developers/docs/topics/opcodes-and-status-codes#json-json-error-codes). |
||||
|
||||
`response`: An object containing properties that describe the error.<br /> |
||||
If the error is a `DiscordHTTPError`, the object will have the `error` and `error_description` properties.<br /> |
||||
If the error is a `DiscordRESTError`, the object will have the `message` and `code` (JSON error code. See `code`.) properties. |
||||
|
||||
`message`: If the error is a `DiscordHTTPError`, it will be a string including the status of the HTTP request and the endpoint used.<br /> |
||||
If the error is a `DiscordRESTError`, it will be a string including the error code and it's meaning. |
||||
|
||||
`stack`: The error stack trace. |
||||
|
||||
```js |
||||
// error.response for DiscordRESTError |
||||
{ |
||||
message: 'Missing Permissions', |
||||
code: 50013 |
||||
} |
||||
``` |
||||
|
||||
```js |
||||
// error.response for DiscordHTTPError |
||||
{ |
||||
error: 'invalid_request', |
||||
error_description: 'Invalid "code" in request.' |
||||
} |
||||
``` |
||||
|
||||
# Contributing |
||||
|
||||
All contributions are welcome. |
@ -0,0 +1,136 @@ |
||||
import { EventEmitter } from "events"; |
||||
|
||||
interface User { |
||||
id: string; |
||||
username: string; |
||||
discriminator: string; |
||||
avatar: string | null | undefined; |
||||
mfa_enabled?: true; |
||||
locale?: string; |
||||
verified?: boolean; |
||||
email?: string | null | undefined; |
||||
flags?: number; |
||||
premium_type?: number; |
||||
public_flags?: number; |
||||
} |
||||
|
||||
interface Member { |
||||
user?: User; |
||||
nick: string | null | undefined; |
||||
roles: string[]; |
||||
joined_at: number; |
||||
premium_since?: number | null | undefined; |
||||
deaf: boolean; |
||||
mute: boolean; |
||||
} |
||||
|
||||
// This is not accurate as discord sends a partial object
|
||||
interface Integration { |
||||
id: string; |
||||
name: string; |
||||
type: string; |
||||
enabled: boolean; |
||||
syncing: boolean; |
||||
role_id: string; |
||||
enable_emoticons?: boolean; |
||||
expire_behavior: 0 | 1; |
||||
expire_grace_period: number; |
||||
user?: User; |
||||
account: { |
||||
id: string; |
||||
name: string; |
||||
}; |
||||
synced_at: number; |
||||
subscriber_count: number;
|
||||
revoked: boolean; |
||||
application?: Application; |
||||
} |
||||
|
||||
interface Connection { |
||||
id: string; |
||||
name: string; |
||||
type: string; |
||||
revoked?: string; |
||||
integrations?: Integration[]; |
||||
verified: boolean; |
||||
friend_sync: boolean; |
||||
show_activity: boolean; |
||||
visibility: 0 | 1; |
||||
} |
||||
|
||||
interface Application { |
||||
id: string; |
||||
name: string; |
||||
icon: string | null | undefined; |
||||
description: string; |
||||
summary: string; |
||||
bot?: User; |
||||
} |
||||
|
||||
interface TokenRequestResult { |
||||
access_token: string; |
||||
token_type: string; |
||||
expires_in: number; |
||||
refresh_token: string; |
||||
scope: string; |
||||
} |
||||
|
||||
interface PartialGuild { |
||||
id: string; |
||||
name: string; |
||||
icon: string | null | undefined; |
||||
owner?: boolean; |
||||
permissions?: number; |
||||
features: string[]; |
||||
permissions_new?: string; |
||||
} |
||||
|
||||
declare class OAuth extends EventEmitter { |
||||
constructor(opts?: { |
||||
version?: string, |
||||
clientId?: string, |
||||
redirectUri?: string, |
||||
credentials?: string, |
||||
clientSecret?: string, |
||||
requestTimeout?: number, |
||||
latencyThreshold?: number, |
||||
ratelimiterOffset?: number, |
||||
}); |
||||
on(event: "debug" | "warn", listener: (message: string) => void): this; |
||||
tokenRequest(opts: { |
||||
code?: string, |
||||
scope: string[] | string, |
||||
clientId?: string, |
||||
grantType: "authorization_code" | "refresh_token", |
||||
redirectUri?: string, |
||||
refreshToken?: string, |
||||
clientSecret?: string, |
||||
}): Promise<TokenRequestResult>; |
||||
revokeToken(access_token: string, credentials?: string): Promise<string>; |
||||
getUser(access_token: string): Promise<User>; |
||||
getUserGuilds(access_token: string): Promise<PartialGuild[]>; |
||||
getUserConnections(access_token: string): Promise<Connection[]>; |
||||
addMember(opts: { |
||||
deaf?: boolean, |
||||
mute?: boolean, |
||||
roles?: string[], |
||||
nickname?: string, |
||||
userId: string, |
||||
guildId: string, |
||||
botToken: string, |
||||
accessToken: string, |
||||
}): Promise<Member>; |
||||
generateAuthUrl(opts: { |
||||
scope: string[] | string, |
||||
state?: string, |
||||
clientId?: string, |
||||
prompt?: "consent" | "none", |
||||
redirectUri?: string, |
||||
responseType?: "code" | "token", |
||||
permissions?: number, |
||||
guildId?: string, |
||||
disableGuildSelect?: boolean, |
||||
}): string; |
||||
} |
||||
|
||||
export = OAuth; |
@ -0,0 +1,3 @@ |
||||
"use strict"; |
||||
const OAuth = require("./lib/oauth"); |
||||
module.exports = OAuth; |
@ -0,0 +1,91 @@ |
||||
/* |
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2016-2020 abalabahaha |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
/* eslint-disable no-prototype-builtins */ |
||||
|
||||
"use strict"; |
||||
|
||||
class DiscordHTTPError extends Error { |
||||
constructor(req, res, response, stack) { |
||||
super(); |
||||
|
||||
Object.defineProperty(this, "req", { |
||||
enumerable: false, |
||||
value: req, |
||||
writable: false, |
||||
}); |
||||
Object.defineProperty(this, "res", { |
||||
enumerable: false, |
||||
value: res, |
||||
writable: false, |
||||
}); |
||||
Object.defineProperty(this, "response", { |
||||
enumerable: false, |
||||
value: response, |
||||
writable: false, |
||||
}); |
||||
|
||||
Object.defineProperty(this, "code", { |
||||
value: res.statusCode, |
||||
writable: false, |
||||
}); |
||||
let message = `${this.name}: ${res.statusCode} ${res.statusMessage} on ${req.method} ${req.path}`; |
||||
const errors = this.flattenErrors(response); |
||||
if (errors.length > 0) { |
||||
message += "\n " + errors.join("\n "); |
||||
} |
||||
Object.defineProperty(this, "message", { |
||||
value: message, |
||||
writable: false, |
||||
}); |
||||
|
||||
if (stack) { |
||||
Object.defineProperty(this, "stack", { |
||||
value: this.message + "\n" + stack, |
||||
writable: false, |
||||
}); |
||||
} |
||||
else { |
||||
Error.captureStackTrace(this, DiscordHTTPError); |
||||
} |
||||
} |
||||
|
||||
get name() { |
||||
return this.constructor.name; |
||||
} |
||||
|
||||
flattenErrors(errors, keyPrefix = "") { |
||||
let messages = []; |
||||
for (const fieldName in errors) { |
||||
if (!errors.hasOwnProperty(fieldName) || fieldName === "message" || fieldName === "code") { |
||||
continue; |
||||
} |
||||
if (Array.isArray(errors[fieldName])) { |
||||
messages = messages.concat(errors[fieldName].map((str) => `${keyPrefix + fieldName}: ${str}`)); |
||||
} |
||||
} |
||||
return messages; |
||||
} |
||||
} |
||||
|
||||
module.exports = DiscordHTTPError; |
@ -0,0 +1,103 @@ |
||||
/* |
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2016-2020 abalabahaha |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
/* eslint-disable no-prototype-builtins */ |
||||
|
||||
"use strict"; |
||||
|
||||
class DiscordRESTError extends Error { |
||||
constructor(req, res, response, stack) { |
||||
super(); |
||||
|
||||
Object.defineProperty(this, "req", { |
||||
enumerable: false, |
||||
value: req, |
||||
writable: false, |
||||
}); |
||||
Object.defineProperty(this, "res", { |
||||
enumerable: false, |
||||
value: res, |
||||
writable: false, |
||||
}); |
||||
Object.defineProperty(this, "response", { |
||||
enumerable: false, |
||||
value: response, |
||||
writable: false, |
||||
}); |
||||
|
||||
Object.defineProperty(this, "code", { |
||||
value: +response.code || -1, |
||||
writable: false, |
||||
}); |
||||
|
||||
let message = this.name + ": " + (response.message || "Unknown error"); |
||||
if (response.errors) { |
||||
message += "\n " + this.flattenErrors(response.errors).join("\n "); |
||||
} |
||||
else { |
||||
const errors = this.flattenErrors(response); |
||||
if (errors.length > 0) { |
||||
message += "\n " + errors.join("\n "); |
||||
} |
||||
} |
||||
Object.defineProperty(this, "message", { |
||||
value: message, |
||||
writable: false, |
||||
}); |
||||
|
||||
if (stack) { |
||||
Object.defineProperty(this, "stack", { |
||||
value: this.message + "\n" + stack, |
||||
writable: false, |
||||
}); |
||||
} |
||||
else { |
||||
Error.captureStackTrace(this, DiscordRESTError); |
||||
} |
||||
} |
||||
|
||||
get name() { |
||||
return `${this.constructor.name} [${this.code}]`; |
||||
} |
||||
|
||||
flattenErrors(errors, keyPrefix = "") { |
||||
let messages = []; |
||||
for (const fieldName in errors) { |
||||
if (!errors.hasOwnProperty(fieldName) || fieldName === "message" || fieldName === "code") { |
||||
continue; |
||||
} |
||||
if (errors[fieldName]._errors) { |
||||
messages = messages.concat(errors[fieldName]._errors.map((obj) => `${keyPrefix + fieldName}: ${obj.message}`)); |
||||
} |
||||
else if (Array.isArray(errors[fieldName])) { |
||||
messages = messages.concat(errors[fieldName].map((str) => `${keyPrefix + fieldName}: ${str}`)); |
||||
} |
||||
else if (typeof errors[fieldName] === "object") { |
||||
messages = messages.concat(this.flattenErrors(errors[fieldName], keyPrefix + fieldName + ".")); |
||||
} |
||||
} |
||||
return messages; |
||||
} |
||||
} |
||||
|
||||
module.exports = DiscordRESTError; |
@ -0,0 +1,305 @@ |
||||
/* |
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2016-2020 abalabahaha |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
const DiscordHTTPError = require("../errors/DiscordHTTPError"); |
||||
const DiscordRESTError = require("../errors/DiscordRESTError"); |
||||
const HTTPS = require("https"); |
||||
const SequentialBucket = require("../util/SequentialBucket"); |
||||
const EventEmitter = require("events"); |
||||
|
||||
/** |
||||
* Handles API requests |
||||
*/ |
||||
class RequestHandler extends EventEmitter { |
||||
constructor(options) { |
||||
super(); |
||||
this.version = options.version; |
||||
this.userAgent = `Discord-OAuth2 (https://github.com/reboxer/discord-oauth2, ${require("../../../package.json").version})`; |
||||
this.ratelimits = {}; |
||||
this.requestTimeout = options.requestTimeout; |
||||
this.latencyThreshold = options.latencyThreshold; |
||||
this.latencyRef = { |
||||
latency: 500, |
||||
offset: options.ratelimiterOffset, |
||||
raw: new Array(10).fill(500), |
||||
timeOffset: 0, |
||||
timeOffsets: new Array(10).fill(0), |
||||
lastTimeOffsetCheck: 0, |
||||
}; |
||||
this.globalBlock = false; |
||||
this.readyQueue = []; |
||||
} |
||||
|
||||
globalUnblock() { |
||||
this.globalBlock = false; |
||||
while (this.readyQueue.length > 0) { |
||||
this.readyQueue.shift()(); |
||||
} |
||||
} |
||||
|
||||
// We need this for the Add Guild Member endpoint
|
||||
routefy(url) { |
||||
return url.replace(/\/([a-z-]+)\/(?:[0-9]{17,19})/g, function(match, p) { |
||||
return p === "guilds" ? match : `/${p}/:id`; |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Make an API request |
||||
* @arg {String} method Uppercase HTTP method |
||||
* @arg {String} url URL of the endpoint |
||||
* @arg {Object} options |
||||
* @arg {Object} [options.auth] |
||||
* @arg {String} [options.auth.type] The type of Authorization to use in the header, wheather Basic, Bearer or Bot |
||||
* @arg {String} [options.auth.creds] The credentials used for the authentication (bot or user access token), if Basic, a base64 string with application's credentials must be passed |
||||
* @arg {String} options.contentType The content type to set in the headers of the request |
||||
* @arg {Object} [body] Request payload |
||||
* @returns {Promise<Object>} Resolves with the returned JSON data |
||||
*/ |
||||
request(method, url, body, options, _route, short) { |
||||
const route = _route || this.routefy(url, method); |
||||
|
||||
const _stackHolder = {}; // Preserve async stack
|
||||
Error.captureStackTrace(_stackHolder); |
||||
|
||||
return new Promise((resolve, reject) => { |
||||
let attempts = 0; |
||||
|
||||
const actualCall = (cb) => { |
||||
const headers = { |
||||
"User-Agent": this.userAgent, |
||||
"Content-Type": options.contentType, |
||||
}; |
||||
let data; |
||||
try { |
||||
if (options.auth) { |
||||
headers["Authorization"] = `${options.auth.type} ${options.auth.creds}`; |
||||
} |
||||
if (headers["Content-Type"] === "application/json") { |
||||
data = JSON.stringify(body); |
||||
} |
||||
else { |
||||
data = body; |
||||
} |
||||
} |
||||
catch (err) { |
||||
cb(); |
||||
reject(err); |
||||
return; |
||||
} |
||||
|
||||
const req = HTTPS.request({ |
||||
method: method, |
||||
host: "discord.com", |
||||
path: `/api/${this.version}` + url, |
||||
headers: headers, |
||||
}); |
||||
|
||||
let reqError; |
||||
|
||||
req.once("abort", () => { |
||||
cb(); |
||||
reqError = reqError || new Error(`Request aborted by client on ${method} ${url}`); |
||||
reqError.req = req; |
||||
reject(reqError); |
||||
}).once("error", (err) => { |
||||
reqError = err; |
||||
req.abort(); |
||||
}); |
||||
|
||||
let latency = Date.now(); |
||||
|
||||
req.once("response", (resp) => { |
||||
latency = Date.now() - latency; |
||||
this.latencyRef.raw.push(latency); |
||||
this.latencyRef.latency = this.latencyRef.latency - ~~(this.latencyRef.raw.shift() / 10) + ~~(latency / 10); |
||||
|
||||
const headerNow = Date.parse(resp.headers["date"]); |
||||
if (this.latencyRef.lastTimeOffsetCheck < Date.now() - 5000) { |
||||
const timeOffset = ~~((this.latencyRef.lastTimeOffsetCheck = Date.now()) - headerNow); |
||||
if (this.latencyRef.timeOffset - this.latencyRef.latency >= this.latencyThreshold && timeOffset - this.latencyRef.latency >= this.latencyThreshold) { |
||||
this.emit("warn", new Error(`Your clock is ${this.latencyRef.timeOffset}ms behind Discord's server clock. Please check your connection and system time.`)); |
||||
} |
||||
this.latencyRef.timeOffset = ~~(this.latencyRef.timeOffset - this.latencyRef.timeOffsets.shift() / 10 + timeOffset / 10); |
||||
this.latencyRef.timeOffsets.push(timeOffset); |
||||
} |
||||
|
||||
resp.once("aborted", () => { |
||||
cb(); |
||||
reqError = reqError || new Error(`Request aborted by server on ${method} ${url}`); |
||||
reqError.req = req; |
||||
reject(reqError); |
||||
}); |
||||
|
||||
let response = ""; |
||||
|
||||
const _respStream = resp; |
||||
|
||||
_respStream.on("data", (str) => { |
||||
response += str; |
||||
}).on("error", (err) => { |
||||
reqError = err; |
||||
req.abort(); |
||||
}).once("end", () => { |
||||
const now = Date.now(); |
||||
|
||||
if (resp.headers["x-ratelimit-limit"]) { |
||||
this.ratelimits[route].limit = +resp.headers["x-ratelimit-limit"]; |
||||
} |
||||
|
||||
if (method !== "GET" && (resp.headers["x-ratelimit-remaining"] === undefined || resp.headers["x-ratelimit-limit"] === undefined) && this.ratelimits[route].limit !== 1) { |
||||
this.emit("debug", `Missing ratelimit headers for SequentialBucket(${this.ratelimits[route].remaining}/${this.ratelimits[route].limit}) with non-default limit\n` |
||||
+ `${resp.statusCode} ${resp.headers["content-type"]}: ${method} ${route} | ${resp.headers["cf-ray"]}\n` |
||||
+ "content-type = " + "\n" |
||||
+ "x-ratelimit-remaining = " + resp.headers["x-ratelimit-remaining"] + "\n" |
||||
+ "x-ratelimit-limit = " + resp.headers["x-ratelimit-limit"] + "\n" |
||||
+ "x-ratelimit-reset = " + resp.headers["x-ratelimit-reset"] + "\n" |
||||
+ "x-ratelimit-global = " + resp.headers["x-ratelimit-global"]); |
||||
} |
||||
|
||||
this.ratelimits[route].remaining = resp.headers["x-ratelimit-remaining"] === undefined ? 1 : +resp.headers["x-ratelimit-remaining"] || 0; |
||||
|
||||
if (resp.headers["retry-after"]) { |
||||
if (resp.headers["x-ratelimit-global"]) { |
||||
this.globalBlock = true; |
||||
setTimeout(() => this.globalUnblock(), +resp.headers["retry-after"] || 1); |
||||
} |
||||
else { |
||||
this.ratelimits[route].reset = (+resp.headers["retry-after"] || 1) + now; |
||||
} |
||||
} |
||||
else if (resp.headers["x-ratelimit-reset"]) { |
||||
if ((~route.lastIndexOf("/reactions/:id")) && (+resp.headers["x-ratelimit-reset"] * 1000 - headerNow) === 1000) { |
||||
this.ratelimits[route].reset = Math.max(now + 250 - this.latencyRef.timeOffset, now); |
||||
} |
||||
else { |
||||
this.ratelimits[route].reset = Math.max(+resp.headers["x-ratelimit-reset"] * 1000 - this.latencyRef.timeOffset, now); |
||||
} |
||||
} |
||||
else { |
||||
this.ratelimits[route].reset = now; |
||||
} |
||||
|
||||
if (resp.statusCode !== 429) { |
||||
this.emit("debug", `${body && body.content} ${now} ${route} ${resp.statusCode}: ${latency}ms (${this.latencyRef.latency}ms avg) | ${this.ratelimits[route].remaining}/${this.ratelimits[route].limit} left | Reset ${this.ratelimits[route].reset} (${this.ratelimits[route].reset - now}ms left)`); |
||||
} |
||||
|
||||
if (resp.statusCode >= 300) { |
||||
if (resp.statusCode === 429) { |
||||
this.emit("debug", `${resp.headers["x-ratelimit-global"] ? "Global" : "Unexpected"} 429 (╯°□°)╯︵ ┻━┻: ${response}\n${body && body.content} ${now} ${route} ${resp.statusCode}: ${latency}ms (${this.latencyRef.latency}ms avg) | ${this.ratelimits[route].remaining}/${this.ratelimits[route].limit} left | Reset ${this.ratelimits[route].reset} (${this.ratelimits[route].reset - now}ms left)`); |
||||
if (resp.headers["retry-after"]) { |
||||
setTimeout(() => { |
||||
cb(); |
||||
this.request(method, url, body, options, route, true).then(resolve).catch(reject); |
||||
}, +resp.headers["retry-after"]); |
||||
return; |
||||
} |
||||
else { |
||||
cb(); |
||||
this.request(method, url, body, options, route, true).then(resolve).catch(reject); |
||||
return; |
||||
} |
||||
} |
||||
else if (resp.statusCode === 502 && ++attempts < 4) { |
||||
this.emit("debug", "A wild 502 appeared! Thanks CloudFlare!"); |
||||
setTimeout(() => { |
||||
this.request(method, url, body, options, route, true).then(resolve).catch(reject); |
||||
}, Math.floor(Math.random() * 1900 + 100)); |
||||
return cb(); |
||||
} |
||||
cb(); |
||||
|
||||
if (response.length > 0) { |
||||
if (resp.headers["content-type"] === "application/json") { |
||||
try { |
||||
response = JSON.parse(response); |
||||
} |
||||
catch (err) { |
||||
reject(err); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
let { stack } = _stackHolder; |
||||
if (stack.startsWith("Error\n")) { |
||||
stack = stack.substring(6); |
||||
} |
||||
let err; |
||||
if (response.code) { |
||||
err = new DiscordRESTError(req, resp, response, stack); |
||||
} |
||||
else { |
||||
err = new DiscordHTTPError(req, resp, response, stack); |
||||
} |
||||
reject(err); |
||||
return; |
||||
} |
||||
|
||||
if (response.length > 0) { |
||||
if (resp.headers["content-type"] === "application/json") { |
||||
try { |
||||
response = JSON.parse(response); |
||||
} |
||||
catch (err) { |
||||
cb(); |
||||
reject(err); |
||||
return; |
||||
} |
||||
} |
||||
} |
||||
|
||||
cb(); |
||||
resolve(response); |
||||
}); |
||||
}); |
||||
|
||||
req.setTimeout(this.requestTimeout, () => { |
||||
reqError = new Error(`Request timed out (>${this.requestTimeout}ms) on ${method} ${url}`); |
||||
req.abort(); |
||||
}); |
||||
|
||||
req.end(data); |
||||
}; |
||||
|
||||
if (this.globalBlock && (options.auth)) { |
||||
this.readyQueue.push(() => { |
||||
if (! this.ratelimits[route]) { |
||||
this.ratelimits[route] = new SequentialBucket(1, this.latencyRef); |
||||
} |
||||
this.ratelimits[route].queue(actualCall, short); |
||||
}); |
||||
} |
||||
else { |
||||
if (! this.ratelimits[route]) { |
||||
this.ratelimits[route] = new SequentialBucket(1, this.latencyRef); |
||||
} |
||||
this.ratelimits[route].queue(actualCall, short); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
module.exports = RequestHandler; |
@ -0,0 +1,105 @@ |
||||
/* |
||||
The MIT License (MIT) |
||||
|
||||
Copyright (c) 2016-2020 abalabahaha |
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of |
||||
this software and associated documentation files (the "Software"), to deal in |
||||
the Software without restriction, including without limitation the rights to |
||||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of |
||||
the Software, and to permit persons to whom the Software is furnished to do so, |
||||
subject to the following conditions: |
||||
|
||||
The above copyright notice and this permission notice shall be included in all |
||||
copies or substantial portions of the Software. |
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS |
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR |
||||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER |
||||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN |
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
||||
*/ |
||||
|
||||
"use strict"; |
||||
|
||||
/** |
||||
* Ratelimit requests and release in sequence |
||||
* @prop {Number} limit How many tokens the bucket can consume in the current interval |
||||
* @prop {Number} remaining How many tokens the bucket has left in the current interval |
||||
* @prop {Number} reset Timestamp of next reset |
||||
* @prop {Boolean} processing Whether the queue is being processed |
||||
*/ |
||||
class SequentialBucket { |
||||
/** |
||||
* Construct a SequentialBucket |
||||
* @arg {Number} tokenLimit The max number of tokens the bucket can consume per interval |
||||
* @arg {Object} [latencyRef] An object |
||||
* @arg {Number} latencyRef.latency Interval between consuming tokens |
||||
*/ |
||||
constructor(limit, latencyRef = { latency: 0 }) { |
||||
this.limit = this.remaining = limit; |
||||
this.resetInterval = 0; |
||||
this.reset = 0; |
||||
this.processing = false; |
||||
this.latencyRef = latencyRef; |
||||
this._queue = []; |
||||
} |
||||
|
||||
/** |
||||
* Queue something in the SequentialBucket |
||||
* @arg {Function} func A function to call when a token can be consumed. The function will be passed a callback argument, which must be called to allow the bucket to continue to work |
||||
*/ |
||||
queue(func, short) { |
||||
if (short) { |
||||
this._queue.unshift(func); |
||||
} |
||||
else { |
||||
this._queue.push(func); |
||||
} |
||||
this.check(); |
||||
} |
||||
|
||||
check(override) { |
||||
if (this._queue.length === 0) { |
||||
if (this.processing) { |
||||
clearTimeout(this.processing); |
||||
this.processing = false; |
||||
} |
||||
return; |
||||
} |
||||
if (this.processing && !override) { |
||||
return; |
||||
} |
||||
const now = Date.now(); |
||||
const offset = this.latencyRef.latency + (this.latencyRef.offset || 0); |
||||
if (!this.reset) { |
||||
this.reset = now - offset; |
||||
this.remaining = this.limit; |
||||
} |
||||
else if (this.reset < now - offset) { |
||||
this.reset = now - offset + (this.resetInterval || 0); |
||||
this.remaining = this.limit; |
||||
} |
||||
this.last = now; |
||||
if (this.remaining <= 0) { |
||||
this.processing = setTimeout(() => { |
||||
this.processing = false; |
||||
this.check(true); |
||||
}, Math.max(0, (this.reset || 0) - now) + offset); |
||||
return; |
||||
} |
||||
--this.remaining; |
||||
this.processing = true; |
||||
this._queue.shift()(() => { |
||||
if (this._queue.length > 0) { |
||||
this.check(true); |
||||
} |
||||
else { |
||||
this.processing = false; |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
|
||||
module.exports = SequentialBucket; |
@ -0,0 +1,218 @@ |
||||
"use strict"; |
||||
|
||||
const RequestHandler = require("./eris/rest/RequestHandler"); |
||||
|
||||
/** |
||||
* Make requests to discord's OAuth2 API |
||||
* @extends requestHandler |
||||
*/ |
||||
class OAuth extends RequestHandler { |
||||
/** |
||||
*
|
||||
* @arg {Object} opts |
||||
* @arg {String?} opts.version The version of the Discord API to use. Defaults to v7. |
||||
* @arg {Number} [opts.requestTimeout=15000] A number of milliseconds before requests are considered timed out |
||||
* @arg {Number} [opts.latencyThreshold=30000] The average request latency at which the RequestHandler will start emitting latency errors |
||||
* @arg {Number} [opts.ratelimiterOffset=0] A number of milliseconds to offset the ratelimit timing calculations by |
||||
* @arg {String?} opts.clientId Your application's client id |
||||
* @arg {String?} opts.clientSecret Your application's client secret |
||||
* @arg {String?} opts.redirectUri Your URL redirect uri |
||||
* @arg {String?} opts.credentials Base64 encoding of the UTF-8 encoded credentials string of your application |
||||
*/ |
||||
constructor(opts = {}) { |
||||
super({ |
||||
version: opts.version || "v7", |
||||
requestTimeout: opts.requestTimeout || 15000, |
||||
latencyThreshold: opts.latencyThreshold || 30000, |
||||
ratelimiterOffset: opts.ratelimiterOffset || 0, |
||||
}); |
||||
|
||||
this.client_id = opts.clientId; |
||||
this.client_secret = opts.clientSecret; |
||||
this.redirect_uri = opts.redirectUri; |
||||
|
||||
this.credentials = opts.credentials; |
||||
} |
||||
|
||||
_encode(obj) { |
||||
let string = ""; |
||||
|
||||
for (const [key, value] of Object.entries(obj)) { |
||||
if (!value) continue; |
||||
string += `&${encodeURIComponent(key)}=${encodeURIComponent(value)}`; |
||||
} |
||||
|
||||
return string.substring(1); |
||||
} |
||||
|
||||
/** |
||||
* Exchange the code returned by discord in the query for the user access token |
||||
* If specified, can also use refresh_token to get a new valid token |
||||
* Read discord's OAuth2 documentation for a full example (https://discord.com/developers/docs/topics/oauth2)
|
||||
* @arg {Object} opts The object containing the parameters for the request |
||||
* @arg {String?} opts.clientId Your application's client id |
||||
* @arg {String?} opts.clientSecret Your application's client secret |
||||
* @arg {String} opts.grantType Either authorization_code or refresh_token |
||||
* @arg {String?} opts.code The code from the querystring |
||||
* @arg {String?} opts.refreshToken The user's refresh token |
||||
* @arg {String?} opts.redirectUri Your URL redirect uri |
||||
* @arg {String} opts.scope The scopes requested in your authorization url, space-delimited |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
tokenRequest(opts = {}) { |
||||
const obj = { |
||||
client_id: opts.clientId || this.client_id, |
||||
client_secret: opts.clientSecret || this.client_secret, |
||||
grant_type: undefined, |
||||
code: undefined, |
||||
refresh_token: undefined, |
||||
redirect_uri: opts.redirectUri || this.redirect_uri, |
||||
scope: opts.scope instanceof Array ? opts.scope.join(" ") : opts.scope, |
||||
}; |
||||
|
||||
if (opts.grantType === "authorization_code") { |
||||
obj.code = opts.code; |
||||
obj.grant_type = opts.grantType; |
||||
} |
||||
else if (opts.grantType === "refresh_token") { |
||||
obj.refresh_token = opts.refreshToken; |
||||
obj.grant_type = opts.grantType; |
||||
} |
||||
else throw new Error("Invalid grant_type provided, it must be either authorization_code or refresh_token"); |
||||
|
||||
const encoded_string = this._encode(obj); |
||||
|
||||
return this.request("POST", "/oauth2/token", encoded_string, { |
||||
contentType: "application/x-www-form-urlencoded", |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Revoke the user access token |
||||
* @arg {String} access_token The user access token |
||||
* @arg {String} credentials Base64 encoding of the UTF-8 encoded credentials string of your application |
||||
* @returns {Promise<String>} |
||||
*/ |
||||
revokeToken(access_token, credentials) { |
||||
if (!credentials && !this.credentials) throw new Error("Missing credentials for revokeToken method"); |
||||
return this.request("POST", "/oauth2/token/revoke", `token=${access_token}`, { |
||||
auth: { |
||||
type: "Basic", |
||||
creds: credentials || this.credentials, |
||||
}, |
||||
contentType: "application/x-www-form-urlencoded", |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Request basic user data |
||||
* Requires the `identify` scope |
||||
* @arg {String} access_token The user access token |
||||
* @returns {Promise<Object>} |
||||
*/ |
||||
getUser(access_token) { |
||||
return this.request("GET", "/users/@me", undefined, { |
||||
auth: { |
||||
type: "Bearer", |
||||
creds: access_token, |
||||
}, |
||||
contentType: "application/json", |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Request all the guilds the user is in |
||||
* Requires the `guilds` scope |
||||
* @arg {String} access_token The user access token |
||||
* @returns {Promise<Object[]>} |
||||
*/ |
||||
getUserGuilds(access_token) { |
||||
return this.request("GET", "/users/@me/guilds", undefined, { |
||||
auth: { |
||||
type: "Bearer", |
||||
creds: access_token, |
||||
}, |
||||
contentType: "application/json", |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Request a user's connections |
||||
* Requires the `connections` scope |
||||
* @arg {String} access_token The user access token |
||||
* @returns {Promise<Object[]>} |
||||
*/ |
||||
getUserConnections(access_token) { |
||||
return this.request("GET", "/users/@me/connections", undefined, { |
||||
auth: { |
||||
type: "Bearer", |
||||
creds: access_token, |
||||
}, |
||||
contentType: "application/json", |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
* Force a user to join a guild |
||||
* Requires the `guilds.join` scope |
||||
* @arg {Object} opts |
||||
* @arg {String} opts.guildId The ID of the guild to join |
||||
* @arg {String} opts.userId The ID of the user to be added to the guild |
||||
* @arg {Boolean?} opts.deaf Whether the user is deafened in voice channels |
||||
* @arg {Boolean?} opts.mute Whether the user is muted in voice channels |
||||
* @arg {String?} opts.nickname Value to set users nickname to |
||||
* @arg {String[]?} opts.roles Array of role ids the member is assigned |
||||
* @arg {String} opts.accessToken The user access token |
||||
* @arg {String} opts.botToken The token of the bot used to authenticate |
||||
* @returns {Promise<Object | String>} |
||||
*/ |
||||
addMember(opts) { |
||||
return this.request("PUT", `/guilds/${opts.guildId}/members/${opts.userId}`, { |
||||
deaf: opts.deaf, |
||||
mute: opts.mute, |
||||
nick: opts.nickname, |
||||
roles: opts.roles, |
||||
access_token: opts.accessToken, |
||||
}, { |
||||
auth: { |
||||
type: "Bot", |
||||
creds: opts.botToken, |
||||
}, |
||||
contentType: "application/json", |
||||
}); |
||||
} |
||||
|
||||
/** |
||||
*
|
||||
* @arg {Object} opts |
||||
* @arg {String} opts.clientId Your application's client id |
||||
* @arg {String?} opts.prompt Controls how existing authorizations are handled, either consent or none (for passthrough scopes authorization is always required). |
||||
* @arg {String?} opts.redirectUri Your URL redirect uri |
||||
* @arg {String?} opts.responseType The response type, either code or token (token is for client-side web applications only). Defaults to code |
||||
* @arg {String | Array} opts.scope The scopes for your URL |
||||
* @arg {String?} opts.state A unique cryptographically secure string (https://discord.com/developers/docs/topics/oauth2#state-and-security)
|
||||
* @arg {Number?} opts.permissions The permissions number for the bot invite (only with bot scope) (https://discord.com/developers/docs/topics/permissions)
|
||||
* @arg {String?} opts.guildId The guild id to pre-fill the bot invite (only with bot scope) |
||||
* @arg {Boolean?} opts.disableGuildSelect Disallows the user from changing the guild for the bot invite, either true or false (only with bot scope) |
||||
* @returns {String} |
||||
*/ |
||||
generateAuthUrl(opts = {}) { |
||||
const obj = { |
||||
client_id: opts.clientId || this.client_id, |
||||
prompt: opts.prompt || undefined, |
||||
redirect_uri: opts.redirectUri || this.redirect_uri, |
||||
response_type: opts.responseType || "code", |
||||
scope: opts.scope instanceof Array ? opts.scope.join(" ") : opts.scope, |
||||
permissions: opts.permissions || undefined, |
||||
guild_id: opts.guildId || undefined, |
||||
disable_guild_select: opts.disableGuildSelect || undefined, |
||||
state: opts.state || undefined, |
||||
}; |
||||
|
||||
const encoded_string = this._encode(obj); |
||||
|
||||
return `https://discord.com/api/oauth2/authorize?${encoded_string}`; |
||||
} |
||||
} |
||||
|
||||
module.exports = OAuth; |
@ -0,0 +1,56 @@ |
||||
{ |
||||
"_from": "discord-oauth2", |
||||
"_id": "discord-oauth2@2.7.1", |
||||
"_inBundle": false, |
||||
"_integrity": "sha512-8PiGsieFxujS6FqcDrWtrunhy5LQGIZC6n1Bi58CP/1/rusqxplj3tXMQ2DBZtw/oyDUrJwSBsYGSr7IJJwTeg==", |
||||
"_location": "/discord-oauth2", |
||||
"_phantomChildren": {}, |
||||
"_requested": { |
||||
"type": "tag", |
||||
"registry": true, |
||||
"raw": "discord-oauth2", |
||||
"name": "discord-oauth2", |
||||
"escapedName": "discord-oauth2", |
||||
"rawSpec": "", |
||||
"saveSpec": null, |
||||
"fetchSpec": "latest" |
||||
}, |
||||
"_requiredBy": [ |
||||
"#USER", |
||||
"/" |
||||
], |
||||
"_resolved": "https://registry.npmjs.org/discord-oauth2/-/discord-oauth2-2.7.1.tgz", |
||||
"_shasum": "d71958d6f321c7bebec859ee60fc316fefef7eb7", |
||||
"_spec": "discord-oauth2", |
||||
"_where": "/home/sigonasr2/divar/server2", |
||||
"author": { |
||||
"name": "reboxer" |
||||
}, |
||||
"bugs": { |
||||
"url": "https://github.com/reboxer/discord-oauth2/issues" |
||||
}, |
||||
"bundleDependencies": false, |
||||
"deprecated": false, |
||||
"description": "Easily interact with discord's oauth2 API", |
||||
"devDependencies": { |
||||
"eslint": "7.30.0" |
||||
}, |
||||
"homepage": "https://github.com/reboxer/discord-oauth2#readme", |
||||
"keywords": [ |
||||
"api", |
||||
"discord", |
||||
"discordapp", |
||||
"oauth2" |
||||
], |
||||
"license": "MIT", |
||||
"main": "index.js", |
||||
"name": "discord-oauth2", |
||||
"repository": { |
||||
"type": "git", |
||||
"url": "git+https://github.com/reboxer/discord-oauth2.git" |
||||
}, |
||||
"scripts": { |
||||
"lint": "eslint --ext .js ./" |
||||
}, |
||||
"version": "2.7.1" |
||||
} |
@ -1,11 +0,0 @@ |
||||
{ |
||||
"name": "secrethash", |
||||
"version": "1.0.0", |
||||
"main": "secrethash.js", |
||||
"scripts": { |
||||
"test": "echo \"Error: no test specified\" && exit 1" |
||||
}, |
||||
"author": "", |
||||
"license": "ISC", |
||||
"description": "" |
||||
} |
Loading…
Reference in new issue