From 57adf744909ec911d7a99d8c8d04df1f30189e7b Mon Sep 17 00:00:00 2001 From: thislooksfun Date: Sun, 1 May 2022 18:22:45 -0500 Subject: [PATCH] refactor: add separate controls for MyUser BREAKING CHANGE: `Client.users.fetchMe()` is now `Client.me.fetch()`. --- examples/oauth/index.ts | 2 +- src/client.ts | 6 +- src/reddit/index.ts | 16 +-- src/reddit/user/base/controls.ts | 86 +++++++++++++ .../{object/base-object.ts => base/object.ts} | 12 +- src/reddit/user/controls.ts | 115 ------------------ src/reddit/user/my-user/controls.ts | 24 ++++ .../{object/my-user.ts => my-user/object.ts} | 32 +++-- src/reddit/user/other-user/controls.ts | 27 ++++ .../other-user.ts => other-user/object.ts} | 21 +++- 10 files changed, 198 insertions(+), 143 deletions(-) create mode 100644 src/reddit/user/base/controls.ts rename src/reddit/user/{object/base-object.ts => base/object.ts} (94%) delete mode 100644 src/reddit/user/controls.ts create mode 100644 src/reddit/user/my-user/controls.ts rename src/reddit/user/{object/my-user.ts => my-user/object.ts} (88%) create mode 100644 src/reddit/user/other-user/controls.ts rename src/reddit/user/{object/other-user.ts => other-user/object.ts} (55%) diff --git a/examples/oauth/index.ts b/examples/oauth/index.ts index ceb09d83..49544072 100644 --- a/examples/oauth/index.ts +++ b/examples/oauth/index.ts @@ -60,7 +60,7 @@ app.get("/auth", async (req, res) => { ); // Get and display the user's information so we know it worked! - const user = await client.users.fetchMe(); + const user = await client.me.fetch(); res.send(`Hello ${user.name}!`); } catch (e) { // If something goes wrong, log it. diff --git a/src/client.ts b/src/client.ts index 4044cdf8..bb336722 100644 --- a/src/client.ts +++ b/src/client.ts @@ -10,7 +10,8 @@ import { makeDebug } from "./helper/debug"; import { CommentControls } from "./reddit/comment/controls"; import { PostControls } from "./reddit/post/controls"; import { SubredditControls } from "./reddit/subreddit/controls"; -import { UserControls } from "./reddit/user/controls"; +import { MyUserControls } from "./reddit/user/my-user/controls"; +import { UserControls } from "./reddit/user/other-user/controls"; const debug = makeDebug("class:Client"); @@ -125,6 +126,8 @@ export interface ClientOptions { export class Client { /** Controls for interacting with comments. */ public readonly comments: CommentControls; + /** Controls for interacting with the currently authorized user. */ + public readonly me: MyUserControls; /** Controls for interacting with posts. */ public readonly posts: PostControls; /** Controls for interacting with subreddits. */ @@ -182,6 +185,7 @@ export class Client { // Set up controls after we have initialized the internal state. this.comments = new CommentControls(this); + this.me = new MyUserControls(this); this.posts = new PostControls(this); this.subreddits = new SubredditControls(this); this.users = new UserControls(this); diff --git a/src/reddit/index.ts b/src/reddit/index.ts index 256000d2..c4b60b15 100644 --- a/src/reddit/index.ts +++ b/src/reddit/index.ts @@ -28,19 +28,21 @@ export type { SubredditData } from "./subreddit/object"; export { Subreddit } from "./subreddit/object"; export type { SubredditType } from "./subreddit/types"; export type { SearchSort, SearchSyntax, Size, TimeRange } from "./types"; -export { UserControls } from "./user/controls"; +export { BaseUserControls } from "./user/base/controls"; +export type { UserData } from "./user/base/object"; +export { User } from "./user/base/object"; export type { BannedUserData } from "./user/moderator-actioned/banned"; export { BannedUser } from "./user/moderator-actioned/banned"; export type { ModeratorActionedUserData } from "./user/moderator-actioned/base"; export { ModeratorActionedUser } from "./user/moderator-actioned/base"; export type { ModeratorData } from "./user/moderator-actioned/moderator"; export { Moderator } from "./user/moderator-actioned/moderator"; -export type { UserData } from "./user/object/base-object"; -export { User } from "./user/object/base-object"; -export type { MyUserData } from "./user/object/my-user"; -export { MyUser } from "./user/object/my-user"; -export type { OtherUserData } from "./user/object/other-user"; -export { OtherUser } from "./user/object/other-user"; +export { MyUserControls } from "./user/my-user/controls"; +export type { MyUserData } from "./user/my-user/object"; +export { MyUser } from "./user/my-user/object"; +export { UserControls } from "./user/other-user/controls"; +export type { OtherUserData } from "./user/other-user/object"; +export { OtherUser } from "./user/other-user/object"; export type { UserItemsSort } from "./user/types"; export { VoteableControls } from "./voteable/controls"; export type { Gildings, VoteableData } from "./voteable/object"; diff --git a/src/reddit/user/base/controls.ts b/src/reddit/user/base/controls.ts new file mode 100644 index 00000000..fcf23089 --- /dev/null +++ b/src/reddit/user/base/controls.ts @@ -0,0 +1,86 @@ +import type { Client } from "../../../client"; +import type { Comment } from "../../comment/object"; +import type { Listing } from "../../listing/listing"; +import type { Post } from "../../post/object"; +import type { PostSort } from "../../post/types"; +import type { Subreddit } from "../../subreddit/object"; +import type { RedditObject } from "../../types"; +import type { MyUserData } from "../my-user/object"; +import type { OtherUserData } from "../other-user/object"; +import type { UserItemsSort } from "../types"; +import type { User, UserData } from "./object"; + +import { BaseControls } from "../../base-controls"; +import { CommentListing } from "../../comment/listing/listing"; +import { fakeListingAfter } from "../../listing/util"; +import { PostListing } from "../../post/listing"; +import { assertKind, fromRedditData } from "../../util"; +import { MyUser } from "../my-user/object"; +import { OtherUser } from "../other-user/object"; + +/** + * The base controls for interacting with users. + * + * @category Controls + */ +export class BaseUserControls extends BaseControls { + /** @internal */ + constructor(client: Client) { + super(client, "u/"); + } + + /** + * Fetch a user's subreddit. + * + * @param username The user who's subreddit to fetch. + * + * @returns A promise that resolves to the user's subreddit. + */ + async fetchSubreddit(username: string): Promise { + return this.client.subreddits.fetch(`u_${username}`); + } + + /** + * Get a Listing of all the posts a user has made. + * + * @param username The user to get posts from. + * @param sort How to sort the posts. + * + * @returns A sorted Listing of posts. + */ + getPosts(username: string, sort: PostSort = "new"): Listing { + const request = { url: `user/${username}/submitted`, query: { sort } }; + const context = { request, client: this.client }; + return new PostListing(fakeListingAfter(""), context); + } + + /** + * Get a Listing of all the comments a user has made. + * + * @param username The user to get comments from. + * @param sort How to sort the comments. + * + * @returns A sorted Listing of comments. + */ + getSortedComments( + username: string, + sort: UserItemsSort = "new" + ): Listing { + const request = { url: `user/${username}/comments`, query: { sort } }; + const context = { request, client: this.client }; + return new CommentListing(fakeListingAfter(""), context); + } + + /** @internal */ + fromRaw(raw: RedditObject): User { + assertKind("t2", raw); + + const rDat = raw.data; + const data: UserData = fromRedditData(rDat); + + // "coins" is only set in the personal user response. + return "coins" in rDat + ? new MyUser(this.client.me, data as MyUserData) + : new OtherUser(this.client.users, data as OtherUserData); + } +} diff --git a/src/reddit/user/object/base-object.ts b/src/reddit/user/base/object.ts similarity index 94% rename from src/reddit/user/object/base-object.ts rename to src/reddit/user/base/object.ts index 6ad406bd..c72890c5 100644 --- a/src/reddit/user/object/base-object.ts +++ b/src/reddit/user/base/object.ts @@ -4,8 +4,8 @@ import type { Listing } from "../../listing/listing"; import type { Post } from "../../post/object"; import type { PostSort } from "../../post/types"; import type { Subreddit } from "../../subreddit/object"; -import type { UserControls } from "../controls"; import type { UserItemsSort } from "../types"; +import type { BaseUserControls } from "./controls"; import { Content } from "../../content"; @@ -44,7 +44,7 @@ export interface UserData extends ContentData { * Whether or not this user is a friend of the authorized user. * * @note This is only provided if you use {@link UserControls.fetch}, *not* if - * you use {@link UserControls.fetchMe}. + * you use {@link MyUserControls.fetch}. */ isFriend?: boolean; @@ -96,10 +96,10 @@ export abstract class User extends Content implements UserData { totalKarma: number; verified: boolean; - protected controls: UserControls; + protected controls: BaseUserControls; /** @internal */ - constructor(controls: UserControls, data: UserData) { + constructor(controls: BaseUserControls, data: UserData) { super(data); this.controls = controls; @@ -127,9 +127,7 @@ export abstract class User extends Content implements UserData { * * @returns A promise that resolves to the newly fetched user. */ - async refetch(): Promise { - return this.controls.fetch(this.name); - } + abstract refetch(): Promise; /** * Fetch the user subreddit for this user. diff --git a/src/reddit/user/controls.ts b/src/reddit/user/controls.ts deleted file mode 100644 index 7ec58ace..00000000 --- a/src/reddit/user/controls.ts +++ /dev/null @@ -1,115 +0,0 @@ -import type { Client } from "../../client"; -import type { Data } from "../../helper/types"; -import type { Comment } from "../comment/object"; -import type { Listing } from "../listing/listing"; -import type { Post } from "../post/object"; -import type { PostSort } from "../post/types"; -import type { Subreddit } from "../subreddit/object"; -import type { RedditObject } from "../types"; -import type { User, UserData } from "./object/base-object"; -import type { MyUserData } from "./object/my-user"; -import type { OtherUserData } from "./object/other-user"; -import type { UserItemsSort } from "./types"; - -import { BaseControls } from "../base-controls"; -import { CommentListing } from "../comment/listing/listing"; -import { fakeListingAfter } from "../listing/util"; -import { PostListing } from "../post/listing"; -import { assertKind, fromRedditData } from "../util"; -import { MyUser } from "./object/my-user"; -import { OtherUser } from "./object/other-user"; - -/** - * Various methods to allow you to interact with users. - * - * @category Controls - */ -export class UserControls extends BaseControls { - /** @internal */ - constructor(client: Client) { - super(client, "u/"); - } - - /** - * Fetch a user from Reddit. - * - * @note If the username you fetch is the same as the authorized user this - * will return a {@link MyUser} instance. Otherwise it will be an instance of - * {@link OtherUser}. To tell dynamically you can use {@link User.isMe}. - * - * @param username The name of the user to fetch. - * - * @returns The user. - */ - async fetch(username: string): Promise { - const raw: RedditObject = await this.gateway.get(`user/${username}/about`); - return this.fromRaw(raw); - } - - /** - * Fetch the details of the authorized user. - * - * @returns The user. - */ - async fetchMe(): Promise { - const userData: Data = await this.gateway.get("api/v1/me"); - // /me doesn't return a wrapped object, so we have to make it ourselves. - const raw: RedditObject = { kind: "t2", data: userData }; - return this.fromRaw(raw) as MyUser; - } - - /** - * Fetch a user's subreddit. - * - * @param username The user who's subreddit to fetch. - * - * @returns A promise that resolves to the user's subreddit. - */ - async fetchSubreddit(username: string): Promise { - return this.client.subreddits.fetch(`u_${username}`); - } - - /** - * Get a Listing of all the posts a user has made. - * - * @param username The user to get posts from. - * @param sort How to sort the posts. - * - * @returns A sorted Listing of posts. - */ - getPosts(username: string, sort: PostSort = "new"): Listing { - const request = { url: `user/${username}/submitted`, query: { sort } }; - const context = { request, client: this.client }; - return new PostListing(fakeListingAfter(""), context); - } - - /** - * Get a Listing of all the comments a user has made. - * - * @param username The user to get comments from. - * @param sort How to sort the comments. - * - * @returns A sorted Listing of comments. - */ - getSortedComments( - username: string, - sort: UserItemsSort = "new" - ): Listing { - const request = { url: `user/${username}/comments`, query: { sort } }; - const context = { request, client: this.client }; - return new CommentListing(fakeListingAfter(""), context); - } - - /** @internal */ - fromRaw(raw: RedditObject): User { - assertKind("t2", raw); - - const rDat = raw.data; - const data: UserData = fromRedditData(rDat); - - // "coins" is only set in the personal user response. - return "coins" in rDat - ? new MyUser(this, data as MyUserData) - : new OtherUser(this, data as OtherUserData); - } -} diff --git a/src/reddit/user/my-user/controls.ts b/src/reddit/user/my-user/controls.ts new file mode 100644 index 00000000..7664076a --- /dev/null +++ b/src/reddit/user/my-user/controls.ts @@ -0,0 +1,24 @@ +import type { Data } from "../../../helper/types"; +import type { RedditObject } from "../../types"; +import type { MyUser } from "./object"; + +import { BaseUserControls } from "../base/controls"; + +/** + * Various methods to allow you to interact with the authorized user. + * + * @category Controls + */ +export class MyUserControls extends BaseUserControls { + /** + * Fetch the details of the authorized user. + * + * @returns The user. + */ + async fetch(): Promise { + const userData: Data = await this.gateway.get("api/v1/me"); + // /me doesn't return a wrapped object, so we have to make it ourselves. + const raw: RedditObject = { kind: "t2", data: userData }; + return this.client.users.fromRaw(raw) as MyUser; + } +} diff --git a/src/reddit/user/object/my-user.ts b/src/reddit/user/my-user/object.ts similarity index 88% rename from src/reddit/user/object/my-user.ts rename to src/reddit/user/my-user/object.ts index 61fa09cb..2c7a4bf7 100644 --- a/src/reddit/user/object/my-user.ts +++ b/src/reddit/user/my-user/object.ts @@ -1,8 +1,8 @@ import type { Maybe } from "../../../helper/types"; -import type { UserControls } from "../controls"; -import type { UserData } from "./base-object"; +import type { UserData } from "../base/object"; +import type { MyUserControls } from "./controls"; -import { User } from "./base-object"; +import { User } from "../base/object"; /** The data for the authorized user. */ export interface MyUserData extends UserData { @@ -116,7 +116,7 @@ export interface MyUserData extends UserData { /** * Whether or not this user has seen the "give award" tooltip. * - * @note This is only set when using {@link UserControls.fetchMe}, *not* when + * @note This is only set when using {@link MyUserControls.fetch}, *not* when * using {@link UserControls.fetch}. */ seenGiveAwardTooltip?: boolean; @@ -124,7 +124,7 @@ export interface MyUserData extends UserData { /** * Whether or not this user has seen the layout switch interface. * - * @note This is only set when using {@link UserControls.fetchMe}, *not* when + * @note This is only set when using {@link MyUserControls.fetch}, *not* when * using {@link UserControls.fetch}. */ seenLayoutSwitch?: boolean; @@ -133,7 +133,7 @@ export interface MyUserData extends UserData { * Whether or not this user has seen a popup saying that buying Reddit premium * lets you disable (some) ads. * - * @note This is only set when using {@link UserControls.fetchMe}, *not* when + * @note This is only set when using {@link MyUserControls.fetch}, *not* when * using {@link UserControls.fetch}. */ seenPremiumAdblockModal?: boolean; @@ -141,7 +141,7 @@ export interface MyUserData extends UserData { /** * Whether or not this user has seen a popup about the redesign. * - * @note This is only set when using {@link UserControls.fetchMe}, *not* when + * @note This is only set when using {@link MyUserControls.fetch}, *not* when * using {@link UserControls.fetch}. */ seenRedesignModal?: boolean; @@ -150,7 +150,7 @@ export interface MyUserData extends UserData { * Whether or not this user has seen the first-time user experience (ftux) * popup about the subreddit chat feature. * - * @note This is only set when using {@link UserControls.fetchMe}, *not* when + * @note This is only set when using {@link MyUserControls.fetch}, *not* when * using {@link UserControls.fetch}. */ seenSubredditChatFtux?: boolean; @@ -202,9 +202,12 @@ export class MyUser extends User implements MyUserData { seenSubredditChatFtux?: boolean; suspensionExpirationUtc: Maybe; + protected override controls: MyUserControls; + /** @internal */ - constructor(controls: UserControls, data: MyUserData) { + constructor(controls: MyUserControls, data: MyUserData) { super(controls, data); + this.controls = controls; this.canCreateSubreddit = data.canCreateSubreddit; this.canEditName = data.canEditName; @@ -242,4 +245,15 @@ export class MyUser extends User implements MyUserData { this.seenSubredditChatFtux = data.seenSubredditChatFtux; this.suspensionExpirationUtc = data.suspensionExpirationUtc; } + + /** + * Re-fetch this user. + * + * Note: This returns a _new object_, it is _not_ mutating. + * + * @returns A promise that resolves to the newly fetched user. + */ + async refetch(): Promise { + return this.controls.fetch(); + } } diff --git a/src/reddit/user/other-user/controls.ts b/src/reddit/user/other-user/controls.ts new file mode 100644 index 00000000..221d6955 --- /dev/null +++ b/src/reddit/user/other-user/controls.ts @@ -0,0 +1,27 @@ +import type { RedditObject } from "../../types"; +import type { User } from "../base/object"; + +import { BaseUserControls } from "../base/controls"; + +/** + * Various methods to allow you to interact with users. + * + * @category Controls + */ +export class UserControls extends BaseUserControls { + /** + * Fetch a user from Reddit. + * + * @note If the username you fetch is the same as the authorized user this + * will return a {@link MyUser} instance. Otherwise it will be an instance of + * {@link OtherUser}. To tell dynamically you can use {@link User.isMe}. + * + * @param username The name of the user to fetch. + * + * @returns The user. + */ + async fetch(username: string): Promise { + const raw: RedditObject = await this.gateway.get(`user/${username}/about`); + return this.fromRaw(raw); + } +} diff --git a/src/reddit/user/object/other-user.ts b/src/reddit/user/other-user/object.ts similarity index 55% rename from src/reddit/user/object/other-user.ts rename to src/reddit/user/other-user/object.ts index df7ebb07..2334bb07 100644 --- a/src/reddit/user/object/other-user.ts +++ b/src/reddit/user/other-user/object.ts @@ -1,7 +1,7 @@ -import type { UserControls } from "../controls"; -import type { UserData } from "./base-object"; +import type { UserData } from "../base/object"; +import type { UserControls } from "./controls"; -import { User } from "./base-object"; +import { User } from "../base/object"; /** The data for a user other than the authorized user. */ export interface OtherUserData extends UserData { @@ -19,11 +19,26 @@ export class OtherUser extends User implements OtherUserData { acceptChats: boolean; acceptPms: boolean; + protected override controls: UserControls; + /** @internal */ constructor(controls: UserControls, data: OtherUserData) { super(controls, data); + this.controls = controls; this.acceptChats = data.acceptChats; this.acceptPms = data.acceptPms; } + + /** + * Re-fetch this user. + * + * Note: This returns a _new object_, it is _not_ mutating. + * + * @returns A promise that resolves to the newly fetched user. + */ + async refetch(): Promise { + const user = await this.controls.fetch(this.name); + return user as OtherUser; + } }