From 7a704f15ccd6b6e64ac65e26593f20f53a0d4779 Mon Sep 17 00:00:00 2001 From: Eric Gustavsson Date: Tue, 3 Sep 2019 14:25:53 +0200 Subject: [PATCH 1/5] Small prorgess on snoowrap.js TS migration --- package-lock.json | 12 ++ package.json | 2 + src/Promise.js | 2 +- src/{create_config.js => create_config.ts} | 1 + src/{snoowrap.js => snoowrap.ts} | 205 +++++++++++++++++++-- 5 files changed, 207 insertions(+), 15 deletions(-) rename src/{create_config.js => create_config.ts} (97%) rename src/{snoowrap.js => snoowrap.ts} (94%) diff --git a/package-lock.json b/package-lock.json index a957fba6..5a6b8d23 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1270,6 +1270,12 @@ } } }, + "@types/bluebird": { + "version": "3.5.27", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.27.tgz", + "integrity": "sha512-6BmYWSBea18+tSjjSC3QIyV93ZKAeNWGM7R6aYt1ryTZXrlHF+QLV0G2yV0viEGVyRkyQsWfMoJ0k/YghBX5sQ==", + "dev": true + }, "@types/caseless": { "version": "0.12.2", "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", @@ -1288,6 +1294,12 @@ "integrity": "sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A==", "dev": true }, + "@types/lodash": { + "version": "4.14.138", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.138.tgz", + "integrity": "sha512-A4uJgHz4hakwNBdHNPdxOTkYmXNgmUAKLbXZ7PKGslgeV0Mb8P3BlbYfPovExek1qnod4pDfRbxuzcVs3dlFLg==", + "dev": true + }, "@types/node": { "version": "12.6.8", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.6.8.tgz", diff --git a/package.json b/package.json index 27a1e5b7..85f050e7 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,8 @@ "@babel/plugin-transform-template-literals": "^7.0.0", "@babel/plugin-transform-typescript": "^7.0.0", "@babel/register": "^7.5.5", + "@types/bluebird": "^3.5.27", + "@types/lodash": "^4.14.138", "@types/request": "^2.48.2", "@typescript-eslint/eslint-plugin": "^1.13.0", "@typescript-eslint/parser": "^1.13.0", diff --git a/src/Promise.js b/src/Promise.js index 22967b3d..b057308b 100644 --- a/src/Promise.js +++ b/src/Promise.js @@ -1,4 +1,4 @@ -import Promise from 'bluebird'; +import {Promise} from 'bluebird'; const PromiseCopy = Promise.getNewLibraryCopy(); PromiseCopy.config({cancellation: true, warnings: false}); diff --git a/src/create_config.js b/src/create_config.ts similarity index 97% rename from src/create_config.js rename to src/create_config.ts index fdb77c59..1ab15fd6 100644 --- a/src/create_config.js +++ b/src/create_config.ts @@ -1,3 +1,4 @@ +// @ts-ignore import {addSnakeCaseShadowProps} from './helpers.js'; export default function () { diff --git a/src/snoowrap.js b/src/snoowrap.ts similarity index 94% rename from src/snoowrap.js rename to src/snoowrap.ts index 91230f6e..ed66dbc5 100644 --- a/src/snoowrap.js +++ b/src/snoowrap.ts @@ -1,9 +1,14 @@ import {defaults, forOwn, includes, isEmpty, map, mapValues, omit, omitBy, snakeCase, values} from 'lodash'; +// @ts-ignore Change when request_handler is changed import Promise from './Promise.js'; +// @ts-ignore Change when request_handler is changed import promiseWrap from 'promise-chains'; -import util from 'util'; +import * as util from 'util'; +// @ts-ignore Change when request_handler is changed import * as requestHandler from './request_handler.js'; +// @ts-ignore import {HTTP_VERBS, KINDS, MAX_LISTING_ITEMS, MODULE_NAME, USER_KEYS, SUBREDDIT_KEYS, VERSION} from './constants.js'; +// @ts-ignore Change when request_handler is changed import * as errors from './errors.js'; import { addEmptyRepliesListing, @@ -13,12 +18,171 @@ import { handleJsonErrors, isBrowser, requiredArg + // @ts-ignore Change when request_handler is changed } from './helpers.js'; import createConfig from './create_config.js'; import * as objects from './objects/index.js'; const api_type = 'json'; + +import { + Comment as _Comment, + Listing as _Listing, + ListingOptions, + SortedListingOptions, + LiveThread as _LiveThread, + LiveThreadSettings, + ModmailConversation as _ModmailConversation, + ModmailConversation, + MultiReddit as _MultiReddit, + MultiRedditProperties, + PrivateMessage as _PrivateMessage, + RedditContent as _RedditContent, + RedditUser as _RedditUser, + ReplyableContent as _ReplyableContent, + Submission as _Submission, + Subreddit as _Subreddit, + SubredditSettings, + VoteableContent as _VoteableContent, + WikiPage as _WikiPage, +} from './objects'; + + +declare namespace Snoowrap { + export interface SnoowrapOptions { + userAgent: string; + clientId?: string; + clientSecret?: string; + username?: string; + password?: string; + refreshToken?: string; + accessToken?: string; + user_agent: string; + client_id?: string; + client_secret?: string; + refresh_token?: string; + access_token?: string; + } + + export interface ConfigOptions { + endpointDomain?: string; + requestDelay?: number; + requestTimeout?: number; + continueAfterRatelimitError?: boolean; + retryErrorCodes?: number[]; + maxRetryAttempts?: number; + warnings?: boolean; + debug?: boolean; + proxies?: boolean; + } + + export interface AuthUrlOptions { + clientId: string; + scope: string[]; + redirectUri: string; + permanent?: boolean; // defaults to true + state?: string; + endpointDomain?: string; + } + + export interface AuthCodeOptions { + code: string; + userAgent: string; + clientId: string; + clientSecret?: string; + redirectUri: string; + endpointDomain?: string; + deviceId?: string; + grantType?: Snoowrap.GrantType + } + + export type Sort = 'confidence' | 'top' | 'new' | 'controversial' | 'old' | 'random' | 'qa'; + + export interface ModAction extends RedditContent { + target_body: string; + mod_id36: string; + created_utc: number; + subreddit: Subreddit; + target_title: string | null; + target_permalink: string; + subreddit_name_prefixed: string; + details: string | null; + action: string; + target_author: string; + target_fullname: string; + sr_id36: string; + id: string; + mod: string; + description: string | null; + } + + export interface SubmitSelfPostOptions { + text?: string; + subredditName: string; + title: string; + sendReplies?: boolean; + captchaIden?: string; + captchaResponse?: string; + } + + export interface SubmitLinkOptions { + url: string; + resubmit?: boolean; + } + + export interface ComposeMessageParams { + to: RedditUser | Subreddit | string; + subject: string; + text: string; + fromSubreddit?: Subreddit | string; + captchaIden?: string; + captchaResponse?: string; + } + + export interface BaseSearchOptions { + query: string; + time?: 'hour' | 'day' | 'week' | 'month' | 'year' | 'all'; + sort?: 'relevance' | 'hot' | 'top' | 'new' | 'comments'; + syntax?: 'cloudsearch' | 'lucene' | 'plain'; + } + + export interface SearchOptions extends BaseSearchOptions { + subreddit?: Subreddit | string; + restrictSr?: boolean; + } + + export interface Trophy { + icon_70: string; + icon_40: string; + name: string; + url: string; + award_id: string; + id: string; + description: string; + } + + export enum GrantType { + CLIENT_CREDENTIALS = 'client_credentials', + INSTALLED_CLIENT = 'https://oauth.reddit.com/grants/installed_client' + } + + export { + _Comment as Comment, + _Listing as Listing, + _LiveThread as LiveThread, + _MultiReddit as MultiReddit, + _PrivateMessage as PrivateMessage, + _RedditContent as RedditContent, + _RedditUser as RedditUser, + _ReplyableContent as ReplyableContent, + _Submission as Submission, + _Subreddit as Subreddit, + _VoteableContent as VoteableContent, + _WikiPage as WikiPage, + }; +} + /** The class for a snoowrap requester. * A requester is the base object that is used to fetch content from reddit. Each requester contains a single set of OAuth tokens. @@ -31,6 +195,17 @@ const api_type = 'json'; exposed since they are useful externally as well. */ const snoowrap = class snoowrap { + accessToken!: string; + clientId!: string; + clientSecret!: string; + password!: string; + ratelimitExpiration!: number; + ratelimitRemaining!: number; + refreshToken!: string; + scope!: string[]; + tokenExpiration!: number; + userAgent!: string; + username!: string; /** * @summary Constructs a new requester. * @desc You should use the snoowrap constructor if you are able to authorize a reddit account in advance (e.g. for a Node.js @@ -76,7 +251,7 @@ const snoowrap = class snoowrap { access_token, accessToken = access_token, username, password - } = {}) { + }: Snoowrap.SnoowrapOptions) { if (!userAgent && !isBrowser) { return requiredArg('userAgent'); } @@ -155,8 +330,8 @@ const snoowrap = class snoowrap { permanent = true, state = '_', endpointDomain = 'reddit.com' - }) { - if (!(Array.isArray(scope) && scope.length && scope.every(scopeValue => scopeValue && typeof scopeValue === 'string'))) { + }: Snoowrap.AuthUrlOptions): string { + if (!(Array.isArray(scope) && scope.length && scope.every(scopeValue => typeof scopeValue === 'string'))) { throw new TypeError('Missing `scope` argument; a non-empty list of OAuth scopes must be provided'); } return ` @@ -215,19 +390,21 @@ const snoowrap = class snoowrap { clientSecret, redirectUri = requiredArg('redirectUri'), endpointDomain = 'reddit.com' - }) { + }: Snoowrap.AuthCodeOptions) { + // @ts-ignore return this.prototype.credentialedClientRequest.call({ userAgent, clientId, clientSecret, // Use `this.prototype.rawRequest` function to allow for custom `rawRequest` method usage in subclasses. + // @ts-ignore rawRequest: this.prototype.rawRequest }, { method: 'post', baseUrl: `https://www.${endpointDomain}/`, uri: 'api/v1/access_token', form: {grant_type: 'authorization_code', code, redirect_uri: redirectUri} - }).then(response => { + }).then((response: any) => { if (response.error) { throw new errors.RequestError(`API Error: ${response.error} - ${response.error_description}`); } @@ -245,11 +422,8 @@ const snoowrap = class snoowrap { * in application-only auth. * @returns {object} The enumeration of possible grant_type values */ - static get grantType () { - return { - CLIENT_CREDENTIALS: 'client_credentials', - INSTALLED_CLIENT: 'https://oauth.reddit.com/grants/installed_client' - }; + static get grantType (): typeof Snoowrap.GrantType { + return Snoowrap.GrantType; } /** * @summary Creates a snoowrap requester from a "user-less" Authorization token @@ -305,23 +479,25 @@ const snoowrap = class snoowrap { deviceId, grantType = snoowrap.grantType.INSTALLED_CLIENT, endpointDomain = 'reddit.com' - }) { + }: Snoowrap.AuthCodeOptions) { if (grantType === snoowrap.grantType.INSTALLED_CLIENT) { if (!deviceId || !(deviceId.length >= 30 && deviceId.length <= 40)) { throw new TypeError('deviceId needs to be between 30 and 40 characters'); } } + // @ts-ignore return this.prototype.credentialedClientRequest.call({ clientId, clientSecret, // Use `this.prototype.rawRequest` function to allow for custom `rawRequest` method usage in subclasses. + // @ts-ignore rawRequest: this.prototype.rawRequest }, { method: 'post', baseUrl: `https://www.${endpointDomain}/`, uri: 'api/v1/access_token', form: {grant_type: grantType, device_id: deviceId} - }).then(response => { + }).then((response: any) => { if (response.error) { throw new errors.RequestError(`API Error: ${response.error} - ${response.error_description}`); } @@ -331,7 +507,8 @@ const snoowrap = class snoowrap { return requester; }); } - _newObject (objectType, content, _hasFetched = false) { + _newObject (objectType: string, content: object[]|object, _hasFetched?: boolean): Array|object { + // @ts-ignore return Array.isArray(content) ? content : new snoowrap.objects[objectType](content, this, _hasFetched); } From 80f21eb9949dc4dd70503b3f65e4c8af60c8ea46 Mon Sep 17 00:00:00 2001 From: Eric Gustavsson Date: Tue, 3 Sep 2019 16:51:15 +0200 Subject: [PATCH 2/5] Small progress on TS refactor --- src/create_config.ts | 12 ++ src/snoowrap.ts | 267 ++++++++++++++++++++----------------------- 2 files changed, 139 insertions(+), 140 deletions(-) diff --git a/src/create_config.ts b/src/create_config.ts index 1ab15fd6..0f8c394f 100644 --- a/src/create_config.ts +++ b/src/create_config.ts @@ -1,6 +1,18 @@ // @ts-ignore import {addSnakeCaseShadowProps} from './helpers.js'; +export interface ConfigOptions { + endpointDomain: string; + requestDelay: number; + requestTimeout: number; + continueAfterRatelimitError: boolean; + retryErrorCodes: number[]; + maxRetryAttempts: number; + warnings: boolean; + debug: boolean; + proxies: boolean; +} + export default function () { const config = Object.create(null); config.endpointDomain = 'reddit.com'; diff --git a/src/snoowrap.ts b/src/snoowrap.ts index ed66dbc5..b70820b2 100644 --- a/src/snoowrap.ts +++ b/src/snoowrap.ts @@ -20,32 +20,31 @@ import { requiredArg // @ts-ignore Change when request_handler is changed } from './helpers.js'; -import createConfig from './create_config.js'; +import createConfig, { ConfigOptions } from './create_config.js'; import * as objects from './objects/index.js'; const api_type = 'json'; import { - Comment as _Comment, - Listing as _Listing, + Comment, + Listing, ListingOptions, SortedListingOptions, - LiveThread as _LiveThread, + LiveThread, LiveThreadSettings, - ModmailConversation as _ModmailConversation, ModmailConversation, - MultiReddit as _MultiReddit, + MultiReddit, MultiRedditProperties, - PrivateMessage as _PrivateMessage, - RedditContent as _RedditContent, - RedditUser as _RedditUser, - ReplyableContent as _ReplyableContent, - Submission as _Submission, - Subreddit as _Subreddit, + PrivateMessage, + RedditContent, + RedditUser, + ReplyableContent, + Submission, + Subreddit, SubredditSettings, - VoteableContent as _VoteableContent, - WikiPage as _WikiPage, + VoteableContent, + WikiPage, } from './objects'; @@ -166,21 +165,6 @@ declare namespace Snoowrap { CLIENT_CREDENTIALS = 'client_credentials', INSTALLED_CLIENT = 'https://oauth.reddit.com/grants/installed_client' } - - export { - _Comment as Comment, - _Listing as Listing, - _LiveThread as LiveThread, - _MultiReddit as MultiReddit, - _PrivateMessage as PrivateMessage, - _RedditContent as RedditContent, - _RedditUser as RedditUser, - _ReplyableContent as ReplyableContent, - _Submission as Submission, - _Subreddit as Subreddit, - _VoteableContent as VoteableContent, - _WikiPage as WikiPage, - }; } /** The class for a snoowrap requester. @@ -206,6 +190,7 @@ const snoowrap = class snoowrap { tokenExpiration!: number; userAgent!: string; username!: string; + private _config!: ConfigOptions; /** * @summary Constructs a new requester. * @desc You should use the snoowrap constructor if you are able to authorize a reddit account in advance (e.g. for a Node.js @@ -507,9 +492,9 @@ const snoowrap = class snoowrap { return requester; }); } - _newObject (objectType: string, content: object[]|object, _hasFetched?: boolean): Array|object { + _newObject(objectType: string, content: T[]|object, _hasFetched?: boolean): T|T[] { // @ts-ignore - return Array.isArray(content) ? content : new snoowrap.objects[objectType](content, this, _hasFetched); + return Array.isArray(content) ? content : new snoowrap.objects[objectType](content, this, _hasFetched) as T; } /** @@ -558,13 +543,13 @@ const snoowrap = class snoowrap { return Object.assign(this._config, options); } - _warn (...args) { + _warn (...args: any[]) { if (this._config.warnings) { console.warn('[warning]', ...args); // eslint-disable-line no-console } } - _debug (...args) { + _debug (...args: any[]) { if (this._config.debug) { console.log('[debug]', ...args); // eslint-disable-line no-console } @@ -585,8 +570,8 @@ const snoowrap = class snoowrap { * r.getUser('not_an_aardvark').link_karma.then(console.log) * // => 6 */ - getUser (name) { - return this._newObject('RedditUser', {name: (name + '').replace(/^\/?u\//, '')}); + getUser (name: string): RedditUser { + return this._newObject('RedditUser', {name: (name + '').replace(/^\/?u\//, '')}) as RedditUser; } /** @@ -600,8 +585,8 @@ const snoowrap = class snoowrap { * r.getComment('c0b6xx0').author.name.then(console.log) * // => 'Kharos' */ - getComment (commentId) { - return this._newObject('Comment', {name: addFullnamePrefix(commentId, 't1_')}); + getComment (commentId: string): Comment { + return this._newObject('Comment', {name: addFullnamePrefix(commentId, 't1_')}) as Comment; } /** @@ -615,8 +600,8 @@ const snoowrap = class snoowrap { * r.getSubreddit('AskReddit').created_utc.then(console.log) * // => 1201233135 */ - getSubreddit (displayName) { - return this._newObject('Subreddit', {display_name: displayName.replace(/^\/?r\//, '')}); + getSubreddit (displayName: string): Subreddit { + return this._newObject('Subreddit', {display_name: displayName.replace(/^\/?r\//, '')}) as Subreddit; } /** @@ -630,8 +615,8 @@ const snoowrap = class snoowrap { * r.getSubmission('2np694').title.then(console.log) * // => 'What tasty food would be distusting if eaten over rice?' */ - getSubmission (submissionId) { - return this._newObject('Submission', {name: addFullnamePrefix(submissionId, 't3_')}); + getSubmission (submissionId: string): Submission { + return this._newObject('Submission', {name: addFullnamePrefix(submissionId, 't3_')}) as Submission; } /** @@ -646,8 +631,8 @@ const snoowrap = class snoowrap { * // => 'Example' * // See here for a screenshot of the PM in question https://i.gyazo.com/24f3b97e55b6ff8e3a74cb026a58b167.png */ - getMessage (messageId) { - return this._newObject('PrivateMessage', {name: addFullnamePrefix(messageId, 't4_')}); + getMessage (messageId: string): PrivateMessage { + return this._newObject('PrivateMessage', {name: addFullnamePrefix(messageId, 't4_')}) as PrivateMessage; } /** @@ -661,8 +646,8 @@ const snoowrap = class snoowrap { * r.getLivethread('whrdxo8dg9n0').nsfw.then(console.log) * // => false */ - getLivethread (threadId) { - return this._newObject('LiveThread', {id: addFullnamePrefix(threadId, 'LiveUpdateEvent_').slice(16)}); + getLivethread (threadId: string): LiveThread { + return this._newObject('LiveThread', {id: addFullnamePrefix(threadId, 'LiveUpdateEvent_').slice(16)}) as LiveThread; } /** @@ -674,8 +659,8 @@ const snoowrap = class snoowrap { * // => RedditUser { is_employee: false, has_mail: false, name: 'snoowrap_testing', ... } */ getMe () { - return this._get({uri: 'api/v1/me'}).then(result => { - this._ownUserInfo = this._newObject('RedditUser', result, true); + return this._get({uri: 'api/v1/me'}).then((result: unknown)=> { + this._ownUserInfo = this._newObject('RedditUser', result as object, true); return this._ownUserInfo; }); } @@ -723,7 +708,7 @@ const snoowrap = class snoowrap { * // => { default_theme_sr: null, threaded_messages: false,hide_downs: true, ... } * // (preferences updated on reddit) */ - updatePreferences (updatedPreferences) { + updatePreferences (updatedPreferences: any): Promise { return this._patch({uri: 'api/v1/me/prefs', body: updatedPreferences}); } @@ -744,7 +729,7 @@ const snoowrap = class snoowrap { * // } * // ] } */ - getMyTrophies () { + getMyTrophies (): Promise { return this._get({uri: 'api/v1/me/trophies'}); } @@ -756,7 +741,7 @@ const snoowrap = class snoowrap { * r.getFriends().then(console.log) * // => [ [ RedditUser { date: 1457927963, name: 'not_an_aardvark', id: 't2_k83md' } ], [] ] */ - getFriends () { + getFriends (): Promise { return this._get({uri: 'prefs/friends'}); } @@ -768,7 +753,7 @@ const snoowrap = class snoowrap { * r.getBlockedUsers().then(console.log) * // => [ RedditUser { date: 1457928120, name: 'actually_an_aardvark', id: 't2_q3519' } ] */ - getBlockedUsers () { + getBlockedUsers (): Promise { return this._get({uri: 'prefs/blocked'}); } @@ -780,7 +765,7 @@ const snoowrap = class snoowrap { * r.checkCaptchaRequirement().then(console.log) * // => false */ - checkCaptchaRequirement () { + checkCaptchaRequirement (): Promise { return this._get({uri: 'api/needs_captcha'}); } @@ -792,7 +777,7 @@ const snoowrap = class snoowrap { * r.getNewCaptchaIdentifier().then(console.log) * // => 'o5M18uy4mk0IW4hs0fu2GNPdXb1Dxe9d' */ - getNewCaptchaIdentifier () { + getNewCaptchaIdentifier (): Promise { return this._post({uri: 'api/new_captcha', form: {api_type}}).then(res => res.json.data.iden); } @@ -805,7 +790,7 @@ const snoowrap = class snoowrap { * r.getCaptchaImage('o5M18uy4mk0IW4hs0fu2GNPdXb1Dxe9d').then(console.log) // => (A long, incoherent string representing the image in PNG format) */ - getCaptchaImage (identifier) { + getCaptchaImage (identifier: string): Promise { return this._get({uri: `captcha/${identifier}`}); } @@ -817,7 +802,7 @@ const snoowrap = class snoowrap { * r.getSavedCategories().then(console.log) * // => [ { category: 'cute cat pictures' }, { category: 'interesting articles' } ] */ - getSavedCategories () { + getSavedCategories (): Promise { return this._get({uri: 'api/saved_categories'}).get('categories'); } @@ -832,7 +817,7 @@ const snoowrap = class snoowrap { * r.markAsVisited(submissions) * // (the links will now appear purple on reddit) */ - markAsVisited (links) { + markAsVisited (links: Submission[]): Promise { return this._post({uri: 'api/store_visits', links: map(links, 'name').join(',')}); } @@ -877,7 +862,7 @@ const snoowrap = class snoowrap { * // => Submission { name: 't3_4abmsz' } * // (new selfpost created on reddit) */ - submitSelfpost (options) { + submitSelfpost (options: Snoowrap.SubmitSelfPostOptions): Promise { return this._submit({...options, kind: 'self'}); } @@ -904,7 +889,7 @@ const snoowrap = class snoowrap { * // => Submission { name: 't3_4abnfe' } * // (new linkpost created on reddit) */ - submitLink (options) { + submitLink (options: Snoowrap.SubmitLinkOptions): Promise { return this._submit({...options, kind: 'link'}); } @@ -920,12 +905,12 @@ const snoowrap = class snoowrap { * @param {boolean} [options.sendReplies=true] Determines whether inbox replies should be enabled for this submission * @param {boolean} [options.resubmit=true] If this is false and same link has already been submitted to this subreddit in the past, reddit will return an error. This could be used to avoid accidental reposts. - * @returns {Promise} The newly-created Submission object + * @returns {Promise} The newly-created Submission object * @example * * await r.submitCrosspost({ title: 'I found an interesting post', originalPost: '6vths0', subredditName: 'snoowrap' }) */ - submitCrosspost (options) { + submitCrosspost (options): Promise { return this._submit({ ...options, kind: 'crosspost', @@ -954,7 +939,7 @@ const snoowrap = class snoowrap { * @param {string} [subredditName] The subreddit to get posts from. If not provided, posts are fetched from the front page of reddit. * @param {object} [options={}] Options for the resulting Listing - * @returns {Promise} A Listing containing the retrieved submissions + * @returns {Promise>} A Listing containing the retrieved submissions * @example * * r.getHot().then(console.log) @@ -976,7 +961,7 @@ const snoowrap = class snoowrap { // Submission { domain: 'self.redditdev', banned_by: null, subreddit: Subreddit { display_name: 'redditdev' }, ...} * // ] */ - getHot (subredditName, options) { + getHot (subredditName?: string, options?: ListingOptions): Promise> { return this._getSortedFrontpage('hot', subredditName, options); } @@ -1007,7 +992,7 @@ const snoowrap = class snoowrap { * @param {string} [subredditName] The subreddit to get posts from. If not provided, posts are fetched from the front page of reddit. * @param {object} [options={}] Options for the resulting Listing - * @returns {Promise} A Listing containing the retrieved submissions + * @returns {Promise>} A Listing containing the retrieved submissions * @example * * r.getNew().then(console.log) @@ -1018,7 +1003,7 @@ const snoowrap = class snoowrap { * // ] * */ - getNew (subredditName, options) { + getNew (subredditName?: string, options?: ListingOptions): Promise> { return this._getSortedFrontpage('new', subredditName, options); } @@ -1027,7 +1012,7 @@ const snoowrap = class snoowrap { * @param {string} [subredditName] The subreddit to get comments from. If not provided, posts are fetched from the front page of reddit. * @param {object} [options={}] Options for the resulting Listing - * @returns {Promise} A Listing containing the retrieved comments + * @returns {Promise>} A Listing containing the retrieved comments * @example * * r.getNewComments().then(console.log) @@ -1036,14 +1021,15 @@ const snoowrap = class snoowrap { * // Comment { link_title: 'How far back in time could you go and still understand English?', ... } * // ] */ - getNewComments (subredditName, options) { + getNewComments (subredditName?: string, options?: ListingOptions): Promise> { return this._getSortedFrontpage('comments', subredditName, options); } /** * @summary Get list of content by IDs. Returns a listing of the requested content. * @param {Array} ids An array of content IDs. Can include the id itself, or a Submission or Comment object. -can get a post and a comment * @returns {Promise>} A listing of content requested, can be any class fetchable by API. e.g. Comment, Submission +can get a post and a comment + * @returns {Promise>} A listing of content requested, can be any class fetchable by API. e.g. Comment, Submission * @example * * r.getContentByIds(['t3_9l9vof','t3_9la341']).then(console.log); @@ -1058,16 +1044,17 @@ can get a post and a comment * @returns {Promise> * // Submission { approved_at_utc: null, ... } * // ] */ - getContentByIds (ids) { + getContentByIds (ids:Array) { if (!Array.isArray(ids)) { throw new TypeError('Invalid argument: Argument needs to be an array.'); } const prefixedIds = ids.map(id => { + // @ts-ignore snoowrap.objects is undefined if (id instanceof snoowrap.objects.Submission || id instanceof snoowrap.objects.Comment) { return id.name; } else if (typeof id === 'string') { - if (!/t(1|3)_/g.test(ids)) { + if (!/t(1|3)_/g.test(id)) { throw new TypeError('Invalid argument: Ids need to include Submission or Comment prefix, e.g. t1_, t3_.'); } return id; @@ -1084,13 +1071,13 @@ can get a post and a comment * @returns {Promise> redirect which cannot be followed by a CORS request. * @param {string} [subredditName] The subreddit to get the random submission. If not provided, the post is fetched from the front page of reddit. - * @returns {Promise} The retrieved Submission object + * @returns {Promise} The retrieved Submission object * @example * * r.getRandomSubmission('aww').then(console.log) * // => Submission { domain: 'i.imgur.com', banned_by: null, subreddit: Subreddit { display_name: 'aww' }, ... } */ - getRandomSubmission (subredditName) { + getRandomSubmission (subredditName?: string): Promise { return this._get({uri: `${subredditName ? `r/${subredditName}/` : ''}random`}); } @@ -1101,7 +1088,7 @@ can get a post and a comment * @returns {Promise> * @param {object} [options={}] Options for the resulting Listing * @param {string} [options.time] Describes the timespan that posts should be retrieved from. Should be one of `hour, day, week, month, year, all` - * @returns {Promise} A Listing containing the retrieved submissions + * @returns {Promise>} A Listing containing the retrieved submissions * @example * * r.getTop({time: 'all', limit: 2}).then(console.log) @@ -1118,7 +1105,7 @@ can get a post and a comment * @returns {Promise> * // ... * // ] */ - getTop (subredditName, options) { + getTop (subredditName?: string, options?: SortedListingOptions): Promise> { return this._getSortedFrontpage('top', subredditName, options); } @@ -1129,7 +1116,7 @@ can get a post and a comment * @returns {Promise> * @param {object} [options={}] Options for the resulting Listing * @param {string} [options.time] Describes the timespan that posts should be retrieved from. Should be one of `hour, day, week, month, year, all` - * @returns {Promise} A Listing containing the retrieved submissions + * @returns {Promise>} A Listing containing the retrieved submissions * @example * * r.getControversial('technology').then(console.log) @@ -1138,7 +1125,7 @@ can get a post and a comment * @returns {Promise> * // Submission { domain: 'pcmag.com', banned_by: null, subreddit: Subreddit { display_name: 'technology' }, ... } * // ] */ - getControversial (subredditName, options) { + getControversial (subredditName?: string, options?: SortedListingOptions): Promise> { return this._getSortedFrontpage('controversial', subredditName, options); } @@ -1147,7 +1134,7 @@ can get a post and a comment * @returns {Promise> * @param {string} [subredditName] The subreddit to get posts from. If not provided, posts are fetched from the front page of reddit. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing the retrieved submissions + * @returns {Promise>} A Listing containing the retrieved submissions * @example * * r.getRising('technology').then(console.log) @@ -1156,14 +1143,14 @@ can get a post and a comment * @returns {Promise> * // Submission { domain: 'pcmag.com', banned_by: null, subreddit: Subreddit { display_name: 'technology' }, ... } * // ] */ - getRising (subredditName, options) { + getRising (subredditName?: string, options?: ListingOptions): Promise> { return this._getSortedFrontpage('rising', subredditName, options); } /** * @summary Gets the authenticated user's unread messages. * @param {object} [options={}] Options for the resulting Listing - * @returns {Promise} A Listing containing unread items in the user's inbox + * @returns {Promise>} A Listing containing unread items in the user's inbox * @example * * r.getUnreadMessages().then(console.log) @@ -1172,7 +1159,7 @@ can get a post and a comment * @returns {Promise> * // Comment { body: 'this is a reply', link_title: 'Yay, a selfpost!', was_comment: true, ... } * // ] */ - getUnreadMessages (options = {}) { + getUnreadMessages (options?: ListingOptions): Promise> { return this._getListing({uri: 'message/unread', qs: options}); } @@ -1191,14 +1178,14 @@ can get a post and a comment * @returns {Promise> * // Comment { body: 'this is a reply', link_title: 'Yay, a selfpost!', was_comment: true, ... } * // ] */ - getInbox ({filter, ...options} = {}) { - return this._getListing({uri: `message/${filter || 'inbox'}`, qs: options}); + getInbox (options?: { filter?: string }): Promise> { + return this._getListing({uri: `message/${options ? options.filter : 'inbox'}`, qs: options}); } /** * @summary Gets the authenticated user's modmail. * @param {object} [options={}] Options for the resulting Listing - * @returns {Promise} A Listing of the user's modmail + * @returns {Promise>} A Listing of the user's modmail * @example * * r.getModmail({limit: 2}).then(console.log) @@ -1207,7 +1194,7 @@ can get a post and a comment * @returns {Promise> * // PrivateMessage { body: '/u/not_an_aardvark has been invited by /u/actually_an_aardvark to ...', ... } * // ] */ - getModmail (options = {}) { + getModmail (options?: ListingOptions): Promise> { return this._getListing({uri: 'message/moderator', qs: options}); } @@ -1223,7 +1210,7 @@ can get a post and a comment * @returns {Promise> * // ModmailConversation { messages: [...], objIds: [...], subject: 'test subject', ... } * // ] */ - getNewModmailConversations (options = {}) { + getNewModmailConversations (options?: ListingOptions & { entity?: string }): Promise> { return this._getListing({ uri: 'api/mod/conversations', qs: options, _name: 'ModmailConversation', _transform: response => { response.after = null; @@ -1265,11 +1252,8 @@ can get a post and a comment * @returns {Promise> * }).then(console.log) * // ModmailConversation { messages: [...], objIds: [...], subject: 'test subject', ... } */ - createModmailDiscussion ({ - body, - subject, - srName - }) { + createModmailDiscussion (options: { body: string, subject: string, srName: string }): Promise { + const {body, subject, srName} = options; const parsedFromSr = srName.replace(/^\/?r\//, ''); // Convert '/r/subreddit_name' to 'subreddit_name' // _newObject ignores most of the response, no practical way to parse the returned content yet return this._post({ @@ -1288,18 +1272,19 @@ can get a post and a comment * @returns {Promise> * r.getNewModmailConversation('75hxt').then(console.log) * // ModmailConversation { messages: [...], objIds: [...], ... } */ - getNewModmailConversation (id) { - return this._newObject('ModmailConversation', {id}); + getNewModmailConversation (id: string): Promise{ + return this._newObject('ModmailConversation', {id}); } /** * @summary Marks all conversations in array as read. * @param {ModmailConversation[]} conversations to mark as read + * @returns {Promise} * @example * * r.markNewModmailConversationsAsRead(['pics', 'sweden']) */ - markNewModmailConversationsAsRead (conversations) { + markNewModmailConversationsAsRead (conversations: ModmailConversation[]): Promise { const conversationIds = conversations.map(message => addFullnamePrefix(message, '')); return this._post({uri: 'api/mod/conversations/read', form: {conversationIds: conversationIds.join(',')}}); } @@ -1307,11 +1292,12 @@ can get a post and a comment * @returns {Promise> /** * @summary Marks all conversations in array as unread. * @param {ModmailConversation[]} conversations to mark as unread + * @returns {Promise} * @example * * r.markNewModmailConversationsAsUnread(['pics', 'sweden']) */ - markNewModmailConversationsAsUnread (conversations) { + markNewModmailConversationsAsUnread (conversations: ModmailConversation[]): Promise { const conversationIds = conversations.map(message => addFullnamePrefix(message, '')); return this._post({uri: 'api/mod/conversations/unread', form: {conversationIds: conversationIds.join(',')}}); } @@ -1327,7 +1313,7 @@ can get a post and a comment * @returns {Promise> * // Subreddit { display_name: 'EarthPorn', ... }, * // ] */ - getNewModmailSubreddits () { + getNewModmailSubreddits (): Promise> { return this._get({uri: 'api/mod/conversations/subreddits'}).then(response => { return Object.values(response.subreddits).map(s => { return this._newObject('Subreddit', s); @@ -1362,7 +1348,7 @@ can get a post and a comment * @returns {Promise> * // mod: 1, * // } */ - getUnreadNewModmailConversationsCount () { + getUnreadNewModmailConversationsCount (): Promise<{ highlighted: number, notifications: number, archived: number, new: number, inprogress: number, mod: number }> { return this._get({uri: 'api/mod/conversations/unread/count'}); } @@ -1385,8 +1371,8 @@ can get a post and a comment * @returns {Promise> * // ModmailConversation { id: '75hxg' } * // ] */ - bulkReadNewModmail (subreddits, state) { - const subredditNames = subreddits.map(s => typeof s === 'string' ? s.replace(/^\/?r\//, '') : s.display_name); + bulkReadNewModmail (subreddits: Array, state: 'new'|'inprogress'|'mod'|'notifications'|'archived'|'highlighted'|'all'): Promise> { + const subredditNames: string[] = subreddits.map(s => typeof s === 'string' ? s.replace(/^\/?r\//, '') : s.display_name); return this._post({uri: 'api/mod/conversations/bulk/read', form: { entity: subredditNames.join(','), state @@ -1404,7 +1390,7 @@ can get a post and a comment * @returns {Promise> /** * @summary Gets the user's sent messages. * @param {object} [options={}] options for the resulting Listing - * @returns {Promise} A Listing of the user's sent messages + * @returns {Promise>} A Listing of the user's sent messages * @example * * r.getSentMessages().then(console.log) @@ -1413,7 +1399,7 @@ can get a post and a comment * @returns {Promise> * // PrivateMessage { body: 'you have been banned from posting to ...' ... } * // ] */ - getSentMessages (options = {}) { + getSentMessages (options?: ListingOptions): Promise> { return this._getListing({uri: 'message/sent', qs: options}); } @@ -1422,7 +1408,7 @@ can get a post and a comment * @returns {Promise> * @param {PrivateMessage[]|String[]} messages An Array of PrivateMessage or Comment objects. Can also contain strings representing message or comment IDs. If strings are provided, they are assumed to represent PrivateMessages unless a fullname prefix such as `t1_` is specified. - * @returns {Promise} A Promise that fulfills when the request is complete + * @returns {Promise} A Promise that fulfills when the request is complete * @example * * r.markMessagesAsRead(['51shsd', '51shxv']) @@ -1434,7 +1420,7 @@ can get a post and a comment * @returns {Promise> * // Alternatively, just pass in a comment object directly. * r.markMessagesAsRead([r.getMessage('51shsd'), r.getComment('d3zhb5k')]) */ - markMessagesAsRead (messages) { + markMessagesAsRead (messages: PrivateMessage[] | string[]): Promise { const messageIds = messages.map(message => addFullnamePrefix(message, 't4_')); return this._post({uri: 'api/read_message', form: {id: messageIds.join(',')}}); } @@ -1456,7 +1442,7 @@ can get a post and a comment * @returns {Promise> * // Alternatively, just pass in a comment object directly. * r.markMessagesAsRead([r.getMessage('51shsd'), r.getComment('d3zhb5k')]) */ - markMessagesAsUnread (messages) { + markMessagesAsUnread (messages: PrivateMessage[] | string[]): Promise { const messageIds = messages.map(message => addFullnamePrefix(message, 't4_')); return this._post({uri: 'api/unread_message', form: {id: messageIds.join(',')}}); } @@ -1474,7 +1460,7 @@ can get a post and a comment * @returns {Promise> * // => Listing [] * // (messages marked as 'read' on reddit) */ - readAllMessages () { + readAllMessages (): Promise { return this._post({uri: 'api/read_all_messages'}); } @@ -1506,7 +1492,7 @@ can get a post and a comment * @returns {Promise> subject, text, to - }) { + }: Snoowrap.ComposeMessageParams): Promise { let parsedTo = to; let parsedFromSr = fromSubreddit; if (to instanceof snoowrap.objects.RedditUser) { @@ -1547,7 +1533,7 @@ can get a post and a comment * @returns {Promise> * // ... * // } */ - getOauthScopeList () { + getOauthScopeList (): Promise<{ [key: string]: { description: string; id: string; name: string } }> { return this._get({uri: 'api/v1/scopes'}); } @@ -1561,7 +1547,7 @@ can get a post and a comment * @returns {Promise> * @param {boolean} [options.restrictSr=true] Restricts search results to the given subreddit * @param {string} [options.sort] Determines how the results should be sorted. One of `relevance, hot, top, new, comments` * @param {string} [options.syntax='plain'] Specifies a syntax for the search. One of `cloudsearch, lucene, plain` - * @returns {Promise} A Listing containing the search results. + * @returns {Promise>} A Listing containing the search results. * @example * * r.search({ @@ -1575,7 +1561,7 @@ can get a post and a comment * @returns {Promise> * // ... * // ] */ - search (options) { + search (options: Snoowrap.SearchOptions): Promise> { if (options.subreddit instanceof snoowrap.objects.Subreddit) { options.subreddit = options.subreddit.display_name; } @@ -1604,7 +1590,8 @@ can get a post and a comment * @returns {Promise> * // ... * // ] */ - searchSubredditNames ({exact = false, include_nsfw = true, includeNsfw = include_nsfw, query}) { + searchSubredditNames (options: { query: string; exact?: boolean; includeNsfw?: boolean; }): Promise { + const {query, exact, includeNsfw} = options; return this._post({uri: 'api/search_reddit_names', qs: {exact, include_over_18: includeNsfw, query}}).get('names'); } @@ -1641,7 +1628,7 @@ can get a post and a comment * @returns {Promise> wiki_edit_age, wiki_edit_karma, wikimode = 'modonly' - }) { + }: SubredditSettings) { return this._post({ uri: 'api/site_admin', form: { allow_images, allow_top, api_type, captcha, collapse_deleted_comments, comment_score_hide_mins, description, @@ -1716,7 +1703,7 @@ can get a post and a comment * @returns {Promise> * // => Subreddit { display_name: 'snoowrap_testing2' } * // (/r/snoowrap_testing2 created on reddit) */ - createSubreddit (options) { + createSubreddit (options: SubredditSettings): Promise { return this._createOrEditSubreddit(options); } @@ -1736,14 +1723,15 @@ can get a post and a comment * @returns {Promise> * // ... * // ] */ - searchSubredditTopics ({query}) { + searchSubredditTopics (options: { query: string; }): Promise { + const {query} = options; return this._get({uri: 'api/subreddits_by_topic', qs: {query}}).map(result => this.getSubreddit(result.name)); } /** * @summary Gets a list of subreddits that the currently-authenticated user is subscribed to. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing Subreddits + * @returns {Promise>} A Listing containing Subreddits * @example * * r.getSubscriptions({limit: 2}).then(console.log) @@ -1760,14 +1748,14 @@ can get a post and a comment * @returns {Promise> * // } * // ] */ - getSubscriptions (options) { + getSubscriptions (options?: ListingOptions): Promise> { return this._getListing({uri: 'subreddits/mine/subscriber', qs: options}); } /** * @summary Gets a list of subreddits in which the currently-authenticated user is an approved submitter. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing Subreddits + * @returns {Promise>} A Listing containing Subreddits * @example * * r.getContributorSubreddits().then(console.log) @@ -1780,14 +1768,14 @@ can get a post and a comment * @returns {Promise> * // ] * */ - getContributorSubreddits (options) { + getContributorSubreddits (options?: ListingOptions): Promise> { return this._getListing({uri: 'subreddits/mine/contributor', qs: options}); } /** * @summary Gets a list of subreddits in which the currently-authenticated user is a moderator. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing Subreddits + * @returns {Promise>} A Listing containing Subreddits * @example * * r.getModeratedSubreddits().then(console.log) @@ -1799,7 +1787,7 @@ can get a post and a comment * @returns {Promise> * // } * // ] */ - getModeratedSubreddits (options) { + getModeratedSubreddits (options?: ListingOptions): Promise> { return this._getListing({uri: 'subreddits/mine/moderator', qs: options}); } @@ -1813,9 +1801,8 @@ can get a post and a comment * @returns {Promise> * r.searchSubreddits({query: 'cookies'}).then(console.log) * // => Listing [ Subreddit { ... }, Subreddit { ... }, ...] */ - searchSubreddits (options) { - options.q = options.query; - return this._getListing({uri: 'subreddits/search', qs: omit(options, 'query')}); + searchSubreddits (options: ListingOptions & { query: string }): Promise> { + return this._getListing({uri: 'subreddits/search', qs: {q: options.query, ...omit(options, 'query')}}); } /** @@ -1834,39 +1821,39 @@ can get a post and a comment * @returns {Promise> /** * @summary Gets a list of subreddits, arranged by age. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing Subreddits + * @returns {Promise>} A Listing containing Subreddits * @example * * r.getNewSubreddits().then(console.log) * // => Listing [ Subreddit { ... }, Subreddit { ... }, ...] */ - getNewSubreddits (options) { + getNewSubreddits (options?: ListingOptions): Promise> { return this._getListing({uri: 'subreddits/new', qs: options}); } /** * @summary Gets a list of gold-exclusive subreddits. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing Subreddits + * @returns {Promise>} A Listing containing Subreddits * @example * * r.getGoldSubreddits().then(console.log) * // => Listing [ Subreddit { ... }, Subreddit { ... }, ...] */ - getGoldSubreddits (options) { + getGoldSubreddits (options?: ListingOptions): Promise> { return this._getListing({uri: 'subreddits/gold', qs: options}); } /** * @summary Gets a list of default subreddits. * @param {object} [options] Options for the resulting Listing - * @returns {Promise} A Listing containing Subreddits + * @returns {Promise>} A Listing containing Subreddits * @example * * r.getDefaultSubreddits().then(console.log) * // => Listing [ Subreddit { ... }, Subreddit { ... }, ...] */ - getDefaultSubreddits (options) { + getDefaultSubreddits (options?: ListingOptions): Promise> { return this._getListing({uri: 'subreddits/default', qs: options}); } @@ -1875,7 +1862,7 @@ can get a post and a comment * @returns {Promise> * @desc **Note:** This function will not work when snoowrap is running in a browser, due to an issue with reddit's CORS settings. * @param {string} name The username in question - * @returns {Promise} A Promise that fulfills with a Boolean (`true` or `false`) + * @returns {Promise} A Promise that fulfills with a Boolean (`true` or `false`) * @example * * r.checkUsernameAvailability('not_an_aardvark').then(console.log) @@ -1883,7 +1870,7 @@ can get a post and a comment * @returns {Promise> * r.checkUsernameAvailability('eqwZAr9qunx7IHqzWVeF').then(console.log) * // => true */ - checkUsernameAvailability (name) { + checkUsernameAvailability (name: string): Promise { // The oauth endpoint listed in reddit's documentation doesn't actually work, so just send an unauthenticated request. return this.unauthenticatedRequest({uri: 'api/username_available.json', qs: {user: name}}); } @@ -1895,13 +1882,13 @@ can get a post and a comment * @returns {Promise> * @param {string} [options.description] A descriptions of the thread. 120 characters max * @param {string} [options.resources] Information and useful links related to the thread. 120 characters max * @param {boolean} [options.nsfw=false] Determines whether the thread is Not Safe For Work - * @returns {Promise} A Promise that fulfills with the new LiveThread when the request is complete + * @returns {Promise} A Promise that fulfills with the new LiveThread when the request is complete * @example * * r.createLivethread({title: 'My livethread'}).then(console.log) * // => LiveThread { id: 'wpimncm1f01j' } */ - createLivethread ({title, description, resources, nsfw = false}) { + createLivethread ({title, description, resources, nsfw = false}: LiveThreadSettings): Promise { return this._post({ uri: 'api/live/create', form: {api_type, description, nsfw, resources, title} @@ -1911,23 +1898,23 @@ can get a post and a comment * @returns {Promise> /** * @summary Gets the "happening now" LiveThread, if it exists * @desc This is the LiveThread that is occasionally linked at the top of reddit.com, relating to current events. - * @returns {Promise} A Promise that fulfills with the "happening now" LiveThread if it exists, or rejects with a 404 error + * @returns {Promise} A Promise that fulfills with the "happening now" LiveThread if it exists, or rejects with a 404 error otherwise. * @example r.getCurrentEventsLivethread().then(thread => thread.stream.on('update', console.log)) */ - getStickiedLivethread () { + getStickiedLivethread (): Promise { return this._get({uri: 'api/live/happening_now'}); } /** * @summary Gets the user's own multireddits. - * @returns {Promise} A Promise for an Array containing the requester's MultiReddits. + * @returns {Promise} A Promise for an Array containing the requester's MultiReddits. * @example * * r.getMyMultireddits().then(console.log) * => [ MultiReddit { ... }, MultiReddit { ... }, ... ] */ - getMyMultireddits () { + getMyMultireddits (): Promise { return this._get({uri: 'api/multi/mine', qs: {expand_srs: true}}); } @@ -1938,13 +1925,13 @@ can get a post and a comment * @returns {Promise> * @param {string} options.description A description for the new multireddit, in markdown. * @param {Array} options.subreddits An Array of Subreddit objects (or subreddit names) that this multireddit should compose of * @param {string} [options.visibility='private'] The multireddit's visibility setting. One of `private`, `public`, `hidden`. - * @param {string} [options.icon_name=''] One of `art and design`, `ask`, `books`, `business`, `cars`, `comics`, + * @param {string} [options.icon_name] One of `art and design`, `ask`, `books`, `business`, `cars`, `comics`, `cute animals`, `diy`, `entertainment`, `food and drink`, `funny`, `games`, `grooming`, `health`, `life advice`, `military`, `models pinup`, `music`, `news`, `philosophy`, `pictures and gifs`, `science`, `shopping`, `sports`, `style`, `tech`, `travel`, `unusual stories`, `video`, `None` * @param {string} [options.key_color='#000000'] A six-digit RGB hex color, preceded by '#' * @param {string} [options.weighting_scheme='classic'] One of `classic`, `fresh` - * @returns {Promise} A Promise for the newly-created MultiReddit object + * @returns {Promise} A Promise for the newly-created MultiReddit object * @example * * r.createMultireddit({ @@ -1955,9 +1942,9 @@ can get a post and a comment * @returns {Promise> * => MultiReddit { display_name: 'myMulti', ... } */ createMultireddit ({ - name, description, subreddits, visibility = 'private', icon_name = '', key_color = '#000000', + name, description, subreddits, visibility = 'private', icon_name, key_color = '#000000', weighting_scheme = 'classic' - }) { + }: MultiRedditProperties & { name: string; subreddits: Subreddit[] | string[]}): Promise { return this._post({ uri: 'api/multi', form: { model: JSON.stringify({ @@ -1985,7 +1972,7 @@ can get a post and a comment * @returns {Promise> are made after this one. * @example r.revokeAccessToken(); */ - revokeAccessToken () { + revokeAccessToken (): Promise { return this._revokeToken(this.accessToken).then(() => { this.accessToken = null; this.tokenExpiration = null; @@ -2001,7 +1988,7 @@ can get a post and a comment * @returns {Promise> been accidentally leaked to a third party. * @example r.revokeRefreshToken(); */ - revokeRefreshToken () { + revokeRefreshToken (): Promise { return this._revokeToken(this.refreshToken).then(() => { this.refreshToken = null; this.accessToken = null; // Revoking a refresh token also revokes any associated access tokens. From 5aee23a094fc46d7b7c2a7de34209861e8a34828 Mon Sep 17 00:00:00 2001 From: Eric Gustavsson Date: Tue, 1 Oct 2019 10:57:11 +0200 Subject: [PATCH 3/5] Some progress for snoowrap.js to TS migration --- src/request_handler.d.ts | 8 +++++ src/snoowrap.ts | 77 +++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 16 deletions(-) create mode 100644 src/request_handler.d.ts diff --git a/src/request_handler.d.ts b/src/request_handler.d.ts new file mode 100644 index 00000000..e7a85322 --- /dev/null +++ b/src/request_handler.d.ts @@ -0,0 +1,8 @@ + +import { Options as RequestOptions } from 'request'; + +declare function oauthRequest(options: RequestOptions, attempts: number): Promise; +declare function credentialedClientRequest(options: RequestOptions): Promise; +declare function unauthenticatedRequest(options: RequestOptions): Promise; +declare function updateAccessToken(): Promise; +declare const rawRequest: Promise; \ No newline at end of file diff --git a/src/snoowrap.ts b/src/snoowrap.ts index b70820b2..0ef6803f 100644 --- a/src/snoowrap.ts +++ b/src/snoowrap.ts @@ -46,9 +46,23 @@ import { VoteableContent, WikiPage, } from './objects'; +import { Options as RequestOptions } from 'request'; declare namespace Snoowrap { + // Make sure RequestHandler functions are typed - don't know how this works + class RequestHandler {} + export interface RequestHandler { + _get(options: {[key: string]: any}): Promise; + _post(options: {[key: string]: any}): Promise; + _put(options: {[key: string]: any}): Promise; + _delete(options: {[key: string]: any}): Promise; + _head(options: {[key: string]: any}): Promise; + _patch(options: {[key: string]: any}): Promise; + oauthRequest(options: RequestOptions, attempts: number): Promise; + unauthenticatedRequest(options: RequestOptions): Promise; + } + export interface SnoowrapOptions { userAgent: string; clientId?: string; @@ -165,6 +179,23 @@ declare namespace Snoowrap { CLIENT_CREDENTIALS = 'client_credentials', INSTALLED_CLIENT = 'https://oauth.reddit.com/grants/installed_client' } + + export interface ObjectType { + Comment: typeof Comment; + Listing: typeof Listing; + LiveThread: typeof LiveThread; + ModmailConversation: typeof ModmailConversation; + MultiReddit: typeof MultiReddit; + PrivateMessage: typeof PrivateMessage; + RedditContent: typeof RedditContent; + RedditUser: typeof RedditUser; + ReplyableContent: typeof ReplyableContent; + Submission: typeof Submission; + Subreddit: typeof Subreddit; + VoteableContent: typeof VoteableContent; + WikiPage: typeof WikiPage; + [key: string]: {prototype: any}; + } } /** The class for a snoowrap requester. @@ -178,7 +209,7 @@ declare namespace Snoowrap { [API rules](https://github.com/reddit/reddit/wiki/API).) These properties primarily exist for internal use, but they are exposed since they are useful externally as well. */ -const snoowrap = class snoowrap { +export default class snoowrap extends Snoowrap.RequestHandler { accessToken!: string; clientId!: string; clientSecret!: string; @@ -191,6 +222,11 @@ const snoowrap = class snoowrap { userAgent!: string; username!: string; private _config!: ConfigOptions; + static errors: object[]|undefined; + static version: string = VERSION; + static objects: Snoowrap.ObjectType; + static _previousSnoowrap: snoowrap; + _ownUserInfo: any; /** * @summary Constructs a new requester. * @desc You should use the snoowrap constructor if you are able to authorize a reddit account in advance (e.g. for a Node.js @@ -658,13 +694,13 @@ const snoowrap = class snoowrap { * r.getMe().then(console.log); * // => RedditUser { is_employee: false, has_mail: false, name: 'snoowrap_testing', ... } */ - getMe () { + getMe (): RedditUser { return this._get({uri: 'api/v1/me'}).then((result: unknown)=> { this._ownUserInfo = this._newObject('RedditUser', result as object, true); return this._ownUserInfo; }); } - + _getMyName () { return Promise.resolve(this._ownUserInfo ? this._ownUserInfo.name : this.getMe().get('name')); } @@ -2040,13 +2076,13 @@ can get a post and a comment return responseTree; } - _getListing ({uri, qs = {}, ...options}) { + _getListing({uri, qs = {}, ...options}: {uri: string, qs: {limit?: number}}) { /* When the response type is expected to be a Listing, add a `count` parameter with a very high number. This ensures that reddit returns a `before` property in the resulting Listing to enable pagination. (Aside from the additional parameter, this function is equivalent to snoowrap.prototype._get) */ const mergedQuery = {count: 9999, ...qs}; return qs.limit || !isEmpty(options) - ? this._newObject('Listing', {_query: mergedQuery, _uri: uri, ...options}).fetchMore(qs.limit || MAX_LISTING_ITEMS) + ? (this._newObject>('Listing', {_query: mergedQuery, _uri: uri, ...options}) as Listing).fetchMore(qs.limit || MAX_LISTING_ITEMS) /* This second case is used as a fallback in case the endpoint unexpectedly ends up returning something other than a Listing (e.g. Submission#getRelated, which used to return a Listing but no longer does due to upstream reddit API changes), in which case using fetch_more() as above will throw an error. @@ -2055,7 +2091,7 @@ can get a post and a comment other meta-properties, the function will still end up throwing an error, but there's not really any good way to handle it (predicting upstream changes can only go so far). More importantly, in the limited cases where it's used, the fallback should have no effect on the returned results */ - : this._get({uri, qs: mergedQuery}).then(listing => { + : this._get({uri, qs: mergedQuery}).then((listing: unknown) => { if (Array.isArray(listing)) { listing.filter(item => item.constructor._name === 'Comment').forEach(addEmptyRepliesListing); } @@ -2071,13 +2107,13 @@ can get a post and a comment */ static noConflict () { if (isBrowser) { - global[MODULE_NAME] = this._previousSnoowrap; + (global as any)[MODULE_NAME] = this._previousSnoowrap; } return this; } }; -function identity (value) { +function identity (value: string) { return value; } @@ -2096,23 +2132,33 @@ const classFuncDescriptors = {configurable: true, writable: true}; Object.defineProperties to ensure that the properties are non-enumerable. */ Object.defineProperties(snoowrap.prototype, mapValues(requestHandler, func => ({value: func, ...classFuncDescriptors}))); -HTTP_VERBS.forEach(method => { +// TODO: Move to main class with decorators +HTTP_VERBS.forEach((method: string) => { /* Define method shortcuts for each of the HTTP verbs. i.e. `snoowrap.prototype._post` is the same as `oauth_request` except that the HTTP method defaults to `post`, and the result is promise-wrapped. Use Object.defineProperty to ensure that the properties are non-enumerable. */ Object.defineProperty(snoowrap.prototype, `_${method}`, { - value (options) { + value (options: object) { return this._promiseWrap(this.oauthRequest({...options, method})); }, ...classFuncDescriptors }); }); +function nonEnumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) { + for (const key in classFuncDescriptors) { + if (classFuncDescriptors.hasOwnProperty(key)) { + const config = (classFuncDescriptors as any)[key]; + (descriptor as any)[key] = config; + } + } +}; + /* `objects` will be an object containing getters for each content type, due to the way objects are exported from objects/index.js. To unwrap these getters into direct properties, use lodash.mapValues with an identity function. */ -snoowrap.objects = mapValues(objects, value => value); +snoowrap.objects = mapValues(objects, (value: any) => value); -forOwn(KINDS, value => { - snoowrap.objects[value] = snoowrap.objects[value] || class extends objects.RedditContent { +forOwn(KINDS, (value: string) => { + snoowrap.objects[value] = snoowrap.objects[value] || class extends objects.RedditContent { }; Object.defineProperty(snoowrap.objects[value], '_name', {value, configurable: true}); }); @@ -2125,11 +2171,10 @@ values(snoowrap.objects).concat(snoowrap).map(func => func.prototype).forEach(fu }); snoowrap.errors = errors; -snoowrap.version = VERSION; if (!module.parent && isBrowser) { // check if the code is being run in a browser through browserify, etc. - snoowrap._previousSnoowrap = global[MODULE_NAME]; - global[MODULE_NAME] = snoowrap; + snoowrap._previousSnoowrap = (global as any)[MODULE_NAME]; + (global as any)[MODULE_NAME] = snoowrap; } module.exports = snoowrap; From fa6f91f20550bafbcdb0d2114858c06440937c38 Mon Sep 17 00:00:00 2001 From: Eric Gustavsson Date: Thu, 3 Oct 2019 11:27:56 +0200 Subject: [PATCH 4/5] Got rid of most of the TS errors --- src/objects/ModmailConversation.d.ts | 1 + src/objects/Subreddit.d.ts | 7 ++++ src/snoowrap.ts | 60 ++++++++++++++++------------ 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/src/objects/ModmailConversation.d.ts b/src/objects/ModmailConversation.d.ts index 52fe9bf6..4afa8465 100644 --- a/src/objects/ModmailConversation.d.ts +++ b/src/objects/ModmailConversation.d.ts @@ -73,6 +73,7 @@ export default class ModmailConversation extends RedditContent; isRead(): boolean; diff --git a/src/objects/Subreddit.d.ts b/src/objects/Subreddit.d.ts index e9868941..45972dec 100644 --- a/src/objects/Subreddit.d.ts +++ b/src/objects/Subreddit.d.ts @@ -225,6 +225,13 @@ export interface SubredditSettings { title: string; public_description: string; description: string; + sr?: Subreddit; + captcha?: string; + captcha_iden?: string; + comment_score_hide_mins?: number; + 'header-title': string; + show_media_preview: boolean; + allow_images?: boolean; submit_text?: string; hide_ads?: boolean; lang?: string; diff --git a/src/snoowrap.ts b/src/snoowrap.ts index 0ef6803f..2e1e93d3 100644 --- a/src/snoowrap.ts +++ b/src/snoowrap.ts @@ -47,6 +47,7 @@ import { WikiPage, } from './objects'; import { Options as RequestOptions } from 'request'; +import { any } from 'bluebird'; declare namespace Snoowrap { @@ -135,8 +136,12 @@ declare namespace Snoowrap { subredditName: string; title: string; sendReplies?: boolean; + send_replies?: boolean; captchaIden?: string; captchaResponse?: string; + subreddit_name: string; + captcha_iden?: string; + captcha_response?: string; } export interface SubmitLinkOptions { @@ -149,8 +154,12 @@ declare namespace Snoowrap { subject: string; text: string; fromSubreddit?: Subreddit | string; + captcha?: string captchaIden?: string; captchaResponse?: string; + from_subreddit?: Subreddit | string; + captcha_iden?: string; + captcha_response?: string; } export interface BaseSearchOptions { @@ -216,7 +225,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { password!: string; ratelimitExpiration!: number; ratelimitRemaining!: number; - refreshToken!: string; + refreshToken!: string|null; scope!: string[]; tokenExpiration!: number; userAgent!: string; @@ -273,6 +282,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { username, password }: Snoowrap.SnoowrapOptions) { + super(); if (!userAgent && !isBrowser) { return requiredArg('userAgent'); } @@ -688,13 +698,13 @@ export default class snoowrap extends Snoowrap.RequestHandler { /** * @summary Gets information on the requester's own user profile. - * @returns {RedditUser} A RedditUser object corresponding to the requester's profile + * @returns {Promise} A RedditUser object corresponding to the requester's profile * @example * * r.getMe().then(console.log); * // => RedditUser { is_employee: false, has_mail: false, name: 'snoowrap_testing', ... } */ - getMe (): RedditUser { + getMe (): Promise { return this._get({uri: 'api/v1/me'}).then((result: unknown)=> { this._ownUserInfo = this._newObject('RedditUser', result as object, true); return this._ownUserInfo; @@ -814,7 +824,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { * // => 'o5M18uy4mk0IW4hs0fu2GNPdXb1Dxe9d' */ getNewCaptchaIdentifier (): Promise { - return this._post({uri: 'api/new_captcha', form: {api_type}}).then(res => res.json.data.iden); + return this._post({uri: 'api/new_captcha', form: {api_type}}).then((res: any) => res.json.data.iden); } /** @@ -868,13 +878,13 @@ export default class snoowrap extends Snoowrap.RequestHandler { title, url, subreddit_name, subredditName = subreddit_name - }) { + }: any) { return this._post({ uri: 'api/submit', form: { api_type, captcha: captchaResponse, iden: captchaIden, sendreplies: sendReplies, sr: subredditName, kind, resubmit, crosspost_fullname, text, title, url } - }).tap(handleJsonErrors(this)).then(result => this.getSubmission(result.json.data.id)); + }).tap(handleJsonErrors(this)).then((result: any) => this.getSubmission(result.json.data.id)); } /** @@ -946,7 +956,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { * * await r.submitCrosspost({ title: 'I found an interesting post', originalPost: '6vths0', subredditName: 'snoowrap' }) */ - submitCrosspost (options): Promise { + submitCrosspost (options: {subredditName: string, title: string, originalPost: string|Submission, sendReplies?: boolean, resubmit?: boolean}): Promise { return this._submit({ ...options, kind: 'crosspost', @@ -956,10 +966,10 @@ export default class snoowrap extends Snoowrap.RequestHandler { }); } - _getSortedFrontpage (sortType, subredditName, options = {}) { + _getSortedFrontpage (sortType: string, subredditName?: string, options: {time?: any, [index:string]:any} = {}) { // Handle things properly if only a time parameter is provided but not the subreddit name let opts = options; - let subName = subredditName; + let subName: string|undefined = subredditName; if (typeof subredditName === 'object' && isEmpty(omitBy(opts, option => option === undefined))) { /* In this case, "subredditName" ends up referring to the second argument, which is not actually a name since the user decided to omit that parameter. */ @@ -1004,7 +1014,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { /** * @summary Gets a Listing of best posts. * @param {object} [options={}] Options for the resulting Listing - * @returns {Promise} A Listing containing the retrieved submissions + * @returns {Promise>} A Listing containing the retrieved submissions * @example * * r.getBest().then(console.log) @@ -1019,7 +1029,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { // Submission { domain: 'self.redditdev', banned_by: null, subreddit: Subreddit { display_name: 'redditdev' }, ...} * // ] */ - getBest (options) { + getBest (options: ListingOptions): Promise> { return this._getSortedFrontpage('best', undefined, options); } @@ -1248,7 +1258,7 @@ can get a post and a comment */ getNewModmailConversations (options?: ListingOptions & { entity?: string }): Promise> { return this._getListing({ - uri: 'api/mod/conversations', qs: options, _name: 'ModmailConversation', _transform: response => { + uri: 'api/mod/conversations', qs: options, _name: 'ModmailConversation', _transform: (response: any) => { response.after = null; response.before = null; response.children = []; @@ -1296,7 +1306,7 @@ can get a post and a comment uri: 'api/mod/conversations', form: { body, subject, srName: parsedFromSr } - }).then(res => this._newObject('ModmailConversation', {id: res.conversation.id})); + }).then((res: any) => this._newObject('ModmailConversation', {id: res.conversation.id})); } /** @@ -1350,9 +1360,9 @@ can get a post and a comment * // ] */ getNewModmailSubreddits (): Promise> { - return this._get({uri: 'api/mod/conversations/subreddits'}).then(response => { + return this._get({uri: 'api/mod/conversations/subreddits'}).then((response: any) => { return Object.values(response.subreddits).map(s => { - return this._newObject('Subreddit', s); + return this._newObject('Subreddit', {s}); }); }); } @@ -1412,11 +1422,11 @@ can get a post and a comment return this._post({uri: 'api/mod/conversations/bulk/read', form: { entity: subredditNames.join(','), state - }}).then(res => { + }}).then((res: any) => { return this._newObject('Listing', { after: null, before: null, - children: res.conversation_ids.map(id => { + children: res.conversation_ids.map((id: string) => { return this._newObject('ModmailConversation', {id}); }) }); @@ -1456,8 +1466,8 @@ can get a post and a comment * // Alternatively, just pass in a comment object directly. * r.markMessagesAsRead([r.getMessage('51shsd'), r.getComment('d3zhb5k')]) */ - markMessagesAsRead (messages: PrivateMessage[] | string[]): Promise { - const messageIds = messages.map(message => addFullnamePrefix(message, 't4_')); + markMessagesAsRead (messages: Array): Promise { + const messageIds = messages.map((message: PrivateMessage | string) => addFullnamePrefix(message, 't4_')); return this._post({uri: 'api/read_message', form: {id: messageIds.join(',')}}); } @@ -1478,8 +1488,8 @@ can get a post and a comment * // Alternatively, just pass in a comment object directly. * r.markMessagesAsRead([r.getMessage('51shsd'), r.getComment('d3zhb5k')]) */ - markMessagesAsUnread (messages: PrivateMessage[] | string[]): Promise { - const messageIds = messages.map(message => addFullnamePrefix(message, 't4_')); + markMessagesAsUnread (messages: Array): Promise { + const messageIds = messages.map((message: PrivateMessage | string) => addFullnamePrefix(message, 't4_')); return this._post({uri: 'api/unread_message', form: {id: messageIds.join(',')}}); } @@ -1761,7 +1771,7 @@ can get a post and a comment */ searchSubredditTopics (options: { query: string; }): Promise { const {query} = options; - return this._get({uri: 'api/subreddits_by_topic', qs: {query}}).map(result => this.getSubreddit(result.name)); + return this._get({uri: 'api/subreddits_by_topic', qs: {query}}).map((result: any) => this.getSubreddit(result.name)); } /** @@ -1850,7 +1860,7 @@ can get a post and a comment * r.getPopularSubreddits().then(console.log) * // => Listing [ Subreddit { ... }, Subreddit { ... }, ...] */ - getPopularSubreddits (options) { + getPopularSubreddits (options: ListingOptions & {include_categories: boolean}) { return this._getListing({uri: 'subreddits/popular', qs: options}); } @@ -1928,7 +1938,7 @@ can get a post and a comment return this._post({ uri: 'api/live/create', form: {api_type, description, nsfw, resources, title} - }).tap(handleJsonErrors(this)).then(result => this.getLivethread(result.json.data.id)); + }).tap(handleJsonErrors(this)).then((result: any) => this.getLivethread(result.json.data.id)); } /** @@ -2076,7 +2086,7 @@ can get a post and a comment return responseTree; } - _getListing({uri, qs = {}, ...options}: {uri: string, qs: {limit?: number}}) { + _getListing({uri, qs = {}, ...options}: {uri: string, qs?: ListingOptions & {[any:string]: any}, options?: {[any:string]: any}}) { /* When the response type is expected to be a Listing, add a `count` parameter with a very high number. This ensures that reddit returns a `before` property in the resulting Listing to enable pagination. (Aside from the additional parameter, this function is equivalent to snoowrap.prototype._get) */ From e3ddb23819849fb7b61529939dca165f8efb583b Mon Sep 17 00:00:00 2001 From: Eric Gustavsson Date: Thu, 3 Oct 2019 12:57:47 +0200 Subject: [PATCH 5/5] Fix all remaining errors --- src/objects/Subreddit.d.ts | 16 +++++++------- src/snoowrap.ts | 43 +++++++++++++++++--------------------- 2 files changed, 27 insertions(+), 32 deletions(-) diff --git a/src/objects/Subreddit.d.ts b/src/objects/Subreddit.d.ts index 45972dec..bee8ad0c 100644 --- a/src/objects/Subreddit.d.ts +++ b/src/objects/Subreddit.d.ts @@ -1,4 +1,4 @@ -import { BaseSearchOptions, ModAction, Sort, SubmitLinkOptions, SubmitSelfPostOptions } from '../snoowrap'; +import { Snoowrap } from '../snoowrap'; import Comment from './Comment'; import Listing, { ListingOptions } from './Listing'; import PrivateMessage from './PrivateMessage'; @@ -69,7 +69,7 @@ export default class Subreddit extends RedditContent { submit_text: string; subreddit_type: SubredditType; subscribers: number; - suggested_comment_sort: Sort | null; + suggested_comment_sort: Snoowrap.Sort | null; title: string; url: string; user_can_flair_in_sr: boolean; @@ -115,7 +115,7 @@ export default class Subreddit extends RedditContent { getEdited(options?: ListingOptions & { only?: 'links' | 'comments' }): Promise>; getHot(options?: ListingOptions): Promise>; getLinkFlairTemplates(linkId: string): Promise; - getModerationLog(opts?: ListingOptions & { mods?: string[]; type?: ModActionType}): Promise>; + getModerationLog(opts?: ListingOptions & { mods?: string[]; type?: ModActionType}): Promise>; getModerators(options?: ListingOptions & { name?: string }): RedditUser[]; getModmail(options?: ListingOptions): Promise>; getNewModmailConversations(options?: ListingOptions): Promise>; @@ -153,7 +153,7 @@ export default class Subreddit extends RedditContent { removeModerator(options: { name: string; }): Promise; removeWikiContributor(options: { name: string; }): Promise; revokeModeratorInvite(options: { name: string; }): Promise; - search(options: BaseSearchOptions): Promise>; + search(options: Snoowrap.BaseSearchOptions): Promise>; selectMyFlair(options: { flair_template_id: string; text?: string; }): Promise; setModeratorPermissions(options: { name: string; permissions: ModeratorPermission; }): Promise; setMultipleUserFlairs(flairs: Array<{ @@ -162,8 +162,8 @@ export default class Subreddit extends RedditContent { cssClass: string; }>): Promise; showMyFlair(): Promise; - submitLink(options: SubmitLinkOptions): Promise; - submitSelfPost(options: SubmitSelfPostOptions): Promise; + submitLink(options: Snoowrap.SubmitLinkOptions): Promise; + submitSelfPost(options: Snoowrap.SubmitSelfPostOptions): Promise; subscribe(): Promise; unbanUser(options: { name: string; }): Promise; unmuteUser(options: { name: string; }): Promise; @@ -225,7 +225,7 @@ export interface SubredditSettings { title: string; public_description: string; description: string; - sr?: Subreddit; + sr?: string; captcha?: string; captcha_iden?: string; comment_score_hide_mins?: number; @@ -251,7 +251,7 @@ export interface SubredditSettings { exclude_banned_modqueue?: boolean; public_traffic?: boolean; collapse_deleted_comments?: boolean; - suggested_comment_sort?: Sort; // TODO rename AvailableSorts? + suggested_comment_sort?: Snoowrap.Sort; // TODO rename AvailableSorts? spoilers_enabled?: boolean; } diff --git a/src/snoowrap.ts b/src/snoowrap.ts index 2e1e93d3..67c09d02 100644 --- a/src/snoowrap.ts +++ b/src/snoowrap.ts @@ -47,10 +47,9 @@ import { WikiPage, } from './objects'; import { Options as RequestOptions } from 'request'; -import { any } from 'bluebird'; -declare namespace Snoowrap { +export declare namespace Snoowrap { // Make sure RequestHandler functions are typed - don't know how this works class RequestHandler {} export interface RequestHandler { @@ -219,7 +218,7 @@ declare namespace Snoowrap { exposed since they are useful externally as well. */ export default class snoowrap extends Snoowrap.RequestHandler { - accessToken!: string; + accessToken!: string|null; clientId!: string; clientSecret!: string; password!: string; @@ -227,7 +226,7 @@ export default class snoowrap extends Snoowrap.RequestHandler { ratelimitRemaining!: number; refreshToken!: string|null; scope!: string[]; - tokenExpiration!: number; + tokenExpiration!: number|null; userAgent!: string; username!: string; private _config!: ConfigOptions; @@ -1258,6 +1257,7 @@ can get a post and a comment */ getNewModmailConversations (options?: ListingOptions & { entity?: string }): Promise> { return this._getListing({ + // @ts-ignore _name is somehow not working - need to fix uri: 'api/mod/conversations', qs: options, _name: 'ModmailConversation', _transform: (response: any) => { response.after = null; response.before = null; @@ -1683,7 +1683,7 @@ can get a post and a comment spam_selfposts, spoilers_enabled, sr, submit_link_label, submit_text, submit_text_label, suggested_comment_sort, title, type, wiki_edit_age, wiki_edit_karma, wikimode } - }).then(handleJsonErrors(this.getSubreddit(name || sr))); + }).then(handleJsonErrors(this.getSubreddit(name || sr || ""))); } /** @@ -1990,7 +1990,7 @@ can get a post and a comment createMultireddit ({ name, description, subreddits, visibility = 'private', icon_name, key_color = '#000000', weighting_scheme = 'classic' - }: MultiRedditProperties & { name: string; subreddits: Subreddit[] | string[]}): Promise { + }: MultiRedditProperties & { name: string; subreddits: Array}): Promise { return this._post({ uri: 'api/multi', form: { model: JSON.stringify({ @@ -1998,7 +1998,7 @@ can get a post and a comment description_md: description, icon_name, key_color, - subreddits: subreddits.map(sub => ({name: typeof sub === 'string' ? sub : sub.display_name})), + subreddits: subreddits.map((sub: Subreddit|string) => ({name: typeof sub === 'string' ? sub : sub.display_name})), visibility, weighting_scheme }) @@ -2006,7 +2006,8 @@ can get a post and a comment }); } - _revokeToken (token) { + _revokeToken (token: string) { + // @ts-ignore return this.credentialedClientRequest({uri: 'api/v1/revoke_token', form: {token}, method: 'post'}); } @@ -2019,7 +2020,7 @@ can get a post and a comment * @example r.revokeAccessToken(); */ revokeAccessToken (): Promise { - return this._revokeToken(this.accessToken).then(() => { + return this._revokeToken(this.accessToken || "").then(() => { this.accessToken = null; this.tokenExpiration = null; }); @@ -2035,34 +2036,36 @@ can get a post and a comment * @example r.revokeRefreshToken(); */ revokeRefreshToken (): Promise { - return this._revokeToken(this.refreshToken).then(() => { + return this.refreshToken && this._revokeToken(this.refreshToken).then(() => { this.refreshToken = null; this.accessToken = null; // Revoking a refresh token also revokes any associated access tokens. this.tokenExpiration = null; }); } - _selectFlair ({flair_template_id, link, name, text, subredditName}) { + _selectFlair ({flair_template_id, link, name, text, subredditName}: {flair_template_id: string, link: string, name: string, text: string, subredditName: string}) { if (!flair_template_id) { throw new errors.InvalidMethodCallError('No flair template ID provided'); } - return Promise.resolve(subredditName).then(subName => { + return Promise.resolve(subredditName).then((subName: string) => { return this._post({uri: `r/${subName}/api/selectflair`, form: {api_type, flair_template_id, link, name, text}}); }); } - _assignFlair ({css_class, cssClass = css_class, link, name, text, subreddit_name, subredditName = subreddit_name}) { - return this._promiseWrap(Promise.resolve(subredditName).then(displayName => { + _assignFlair ({css_class, cssClass = css_class, link, name, text, subreddit_name, subredditName = subreddit_name}: {css_class?: string, cssClass?: string, link: string, name: string, text: string, subreddit_name?: string, subredditName?: string}) { + return this._promiseWrap(Promise.resolve(subredditName).then((displayName: string) => { return this._post({uri: `r/${displayName}/api/flair`, form: {api_type, name, text, link, css_class: cssClass}}); })); } - _populate (responseTree) { + _populate (responseTree: {[index:string]: any}|null): object|null { if (typeof responseTree === 'object' && responseTree !== null) { // Map {kind: 't2', data: {name: 'some_username', ... }} to a RedditUser (e.g.) with the same properties if (Object.keys(responseTree).length === 2 && responseTree.kind && responseTree.data) { + // @ts-ignore return this._newObject(KINDS[responseTree.kind] || 'RedditContent', this._populate(responseTree.data), true); } + // @ts-ignore const result = (Array.isArray(responseTree) ? map : mapValues)(responseTree, (value, key) => { // Maps {author: 'some_username'} to {author: RedditUser { name: 'some_username' } } if (value !== null && USER_KEYS.has(key)) { @@ -2130,6 +2133,7 @@ function identity (value: string) { defineInspectFunc(snoowrap.prototype, function () { // Hide confidential information (tokens, client IDs, etc.), as well as private properties, from the console.log output. const keysForHiddenValues = ['clientSecret', 'refreshToken', 'accessToken', 'password']; + // @ts-ignore TODO: this can't be resolved const formatted = mapValues(omitBy(this, (value, key) => typeof key === 'string' && key.startsWith('_')), (value, key) => { return includes(keysForHiddenValues, key) ? value && '(redacted)' : value; }); @@ -2154,15 +2158,6 @@ HTTP_VERBS.forEach((method: string) => { }); }); -function nonEnumerable(target: any, propertyKey: string, descriptor: PropertyDescriptor) { - for (const key in classFuncDescriptors) { - if (classFuncDescriptors.hasOwnProperty(key)) { - const config = (classFuncDescriptors as any)[key]; - (descriptor as any)[key] = config; - } - } -}; - /* `objects` will be an object containing getters for each content type, due to the way objects are exported from objects/index.js. To unwrap these getters into direct properties, use lodash.mapValues with an identity function. */ snoowrap.objects = mapValues(objects, (value: any) => value);