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