Skip to content

Commit

Permalink
feat: add initial support for interacting with users
Browse files Browse the repository at this point in the history
  • Loading branch information
thislooksfun committed May 14, 2021
1 parent 5073ec4 commit bd0fc8b
Show file tree
Hide file tree
Showing 8 changed files with 539 additions and 1 deletion.
10 changes: 9 additions & 1 deletion src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@ import type { Data } from "./helper/types";
import type { OauthOpts } from "./helper/api/oauth";
import type { Query } from "./helper/api/core";
import type { Token } from "./helper/accessToken";
import { CommentControls, PostControls, SubredditControls } from "./controls";
import {
CommentControls,
PostControls,
SubredditControls,
UserControls,
} from "./controls";
import { updateAccessToken, tokenFromCode } from "./helper/accessToken";
import * as anon from "./helper/api/anon";
import * as oauth from "./helper/api/oauth";
Expand Down Expand Up @@ -146,6 +151,8 @@ export default class Client {
public posts: PostControls;
/** Controls for interacting with subreddits. */
public subreddits: SubredditControls;
/** Controls for interacting with users. */
public users: UserControls;

protected auth?: Auth;
protected creds?: Credentials;
Expand All @@ -167,6 +174,7 @@ export default class Client {
this.comments = new CommentControls(this);
this.posts = new PostControls(this);
this.subreddits = new SubredditControls(this);
this.users = new UserControls(this);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/controls/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { default as BaseControls } from "./base";
export { default as SubredditControls } from "./subreddit";
export { default as UserControls } from "./user";
export { default as VoteableControls } from "./voteable";
export { default as CommentControls } from "./comment";
export { default as PostControls } from "./post";
94 changes: 94 additions & 0 deletions src/controls/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { _Listing } from "../listings/listing";
import type { Comment, Post } from "..";
import type { Data, PostSort, RedditObject } from "../helper/types";
import type { MyUserData, OtherUserData, UserData } from "../objects/user";
import type Client from "../client";
import type Listing from "../listings/listing";
import { camelCaseKeys, assertKind } from "../helper/util";
import { fakeListingAfter } from "../listings/util";
import { MyUser, OtherUser, User } from "../objects/user";
import BaseControls from "./base";
import CommentListing from "../listings/comment";
import PostListing from "../listings/post";

/**
* Various methods to allow you to interact with users.
*
* @category Controls
*/
export default 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<User> {
const res: RedditObject = await this.client.get(`user/${username}/about`);
return this.fromRaw(res);
}

/**
* Fetch the details of the authorized user.
*
* @returns The user.
*/
async fetchMe(): Promise<MyUser> {
const res: Data = await this.client.get("api/v1/me");
// /me doesn't return a wrapped object, so we have to make it ourselves.
const raw: RedditObject = { kind: "t2", data: res };
return this.fromRaw(raw) as MyUser;
}

/**
* Get a Listing of all the posts a user has made.
*
* @param user The user to get posts from.
* @param sort How to sort the posts.
*
* @returns A sorted Listing of posts.
*/
getPosts(user: string, sort: PostSort): Listing<Post> {
const req = { url: `user/${user}/submitted`, query: { sort } };
const ctx = { req, client: this.client };
return new PostListing(fakeListingAfter(""), ctx);
}

/**
* Get a Listing of all the comments a user has made.
*
* @param user The user to get comments from.
* @param sort How to sort the comments.
*
* @returns A sorted Listing of comments.
*/
getSortedComments(user: string, sort: string = "new"): Listing<Comment> {
const req = { url: `user/${user}/comments`, query: { sort } };
const ctx = { req, client: this.client };
return new CommentListing(fakeListingAfter(""), ctx);
}

/** @internal */
fromRaw(raw: RedditObject): User {
assertKind("t2", raw);

const rDat = raw.data;
const data: UserData = camelCaseKeys(rDat);

if ("coins" in rDat) {
return new MyUser(this, data as MyUserData);
} else {
return new OtherUser(this, data as OtherUserData);
}
}
}
2 changes: 2 additions & 0 deletions src/objects/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ export * from "./content";
export { default as Subreddit } from "./subreddit";
export * from "./subreddit";

export * from "./user";

export { default as Votable } from "./voteable";
export * from "./voteable";

Expand Down
161 changes: 161 additions & 0 deletions src/objects/user/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
import type { PostSort } from "../../helper/types";
import type { UserControls } from "../../controls";
import { Comment, Content, ContentData, Post, Subreddit } from "..";
import Listing from "../../listings/listing";

// TODO: Suspended users give back a very minimal response.
// BODY: Fetching a suspended user gives back very minimal data, meaning that
// BODY: this typing below is only a valid base for non-suspended users. I'm not
// BODY: sure what the best way to deal with this is...

/** The data for a single Reddit user. */
export interface UserData extends ContentData {
/** The amount of karma this user has gotten from getting awards. */
awardeeKarma: number;

/** The amount of karma this user has gotten from giving awards. */
awarderKarma: number;

/** The total karma this user has gotten from comments. */
commentKarma: number;

/** Whether or not this user has subscribed to at least one subreddit. */
hasSubscribed: boolean;

/** Whether or not this user has a verified email. */
hasVerifiedEmail: boolean;

/** Whether or not this user should be hidden from search engine crawlers. */
hideFromRobots: boolean;

/** The URL of this user's avatar image. */
iconImg: string;

/** Whether or not this user is a Reddit employee. */
isEmployee: boolean;

/**
* 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}.
*/
isFriend?: boolean;

/** Whether or not this user currently has Reddit Premium */
isGold: boolean;

/** Whether or not this user is a moderator somewhere on Reddit. */
isMod: boolean;

/** The total karma this user has gotten from posts. */
linkKarma: number;

/** The user's name. */
name: string;

/** The total karma this user has. */
totalKarma: number;

/** Whether or not this user is verified. ??? */
verified: boolean;
}

/**
* Any Reddit user.
*
* If you need more information you can cast to either {@link MyUser} or, more
* likely {@link OtherUser}. See {@link isMe} for more information.
*/
export abstract class User extends Content implements UserData {
/**
* Whether this user is the authorized user (instanceof {@link MyUser}) or not
* (instanceof {@link OtherUser}).
*/
abstract isMe: boolean;

awardeeKarma: number;
awarderKarma: number;
commentKarma: number;
hasSubscribed: boolean;
hasVerifiedEmail: boolean;
hideFromRobots: boolean;
iconImg: string;
isEmployee: boolean;
isFriend?: boolean;
isGold: boolean;
isMod: boolean;
linkKarma: number;
name: string;
totalKarma: number;
verified: boolean;

protected controls: UserControls;

/** @internal */
constructor(controls: UserControls, data: UserData) {
super(data);
this.controls = controls;

this.awardeeKarma = data.awardeeKarma;
this.awarderKarma = data.awarderKarma;
this.commentKarma = data.commentKarma;
this.hasSubscribed = data.hasSubscribed;
this.hasVerifiedEmail = data.hasVerifiedEmail;
this.hideFromRobots = data.hideFromRobots;
this.iconImg = data.iconImg;
this.isEmployee = data.isEmployee;
this.isFriend = data.isFriend;
this.isGold = data.isGold;
this.isMod = data.isMod;
this.linkKarma = data.linkKarma;
this.name = data.name;
this.totalKarma = data.totalKarma;
this.verified = data.verified;
}

/**
* 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<User> {
return this.controls.fetch(this.name);
}

/**
* Fetch the user subreddit for this user.
*
* @returns A promise that resolves to this user's subreddit.
*/
async fetchSubreddit(): Promise<Subreddit> {
// TODO: The user fetch does return some info about this subreddit, just not
// enough to populate a full Subreddit instance. Is there some way we could
// make use of that partial data?
return this.controls.getClient().subreddits.fetch(`u_${this.name}`);
}

/**
* Get a Listing of all the posts this user has made.
*
* @param sort How to sort the posts.
*
* @returns A sorted Listing of posts.
*/
getPosts(sort: PostSort): Listing<Post> {
return this.controls.getPosts(this.name, sort);
}

/**
* Get a Listing of all the comments this user has made.
*
* @param sort How to sort the comments.
*
* @returns A sorted Listing of comments.
*/
getSortedComments(sort: string = "new"): Listing<Comment> {
return this.controls.getSortedComments(this.name, sort);
}
}
3 changes: 3 additions & 0 deletions src/objects/user/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from "./base";
export * from "./myuser";
export * from "./otheruser";

0 comments on commit bd0fc8b

Please sign in to comment.