Skip to content

Commit 943df37

Browse files
authored
feat: add support for manage lists endpoints (#63)
* feat: add list manager and method for creating/deleting a list * feat(ListManager): add methods to update list and add/remove member * feat(ListManager): add methods to follow and unfollow a list * feat(ListManager): add methods to pin or unpin a list * fix: update twitter-types to use the fixed interface names * feat(List): add description and private property
1 parent ad67f42 commit 943df37

File tree

12 files changed

+275
-11
lines changed

12 files changed

+275
-11
lines changed

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,5 +38,6 @@
3838
},
3939
"typescript.suggest.autoImports": true,
4040
"typescript.referencesCodeLens.enabled": true,
41-
"typescript.implementationsCodeLens.enabled": true
41+
"typescript.implementationsCodeLens.enabled": true,
42+
"typescript.tsdk": "node_modules\\typescript\\lib"
4243
}

package-lock.json

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"jest": "^27.1.0",
5757
"lint-staged": "^11.1.2",
5858
"prettier": "^2.4.1",
59-
"twitter-types": "^0.15.0",
59+
"twitter-types": "^0.15.1",
6060
"typedoc": "^0.22.4",
6161
"typescript": "^4.4.3"
6262
},

src/client/Client.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { RESTManager } from '../rest/RESTManager';
33
import { ClientEvents, StreamType } from '../util';
44
import { CustomError, CustomTypeError } from '../errors';
55
import { SampledTweetStream, FilteredTweetStream } from '../streams';
6-
import { UserManager, TweetManager, SpaceManager } from '../managers';
6+
import { UserManager, TweetManager, SpaceManager, ListManager } from '../managers';
77
import { ClientCredentials, RequestData, ClientUser } from '../structures';
88
import type { Response } from 'undici';
99
import type { ClientCredentialsInterface, ClientOptions } from '../typings';
@@ -64,6 +64,11 @@ export class Client extends BaseClient {
6464
*/
6565
spaces: SpaceManager;
6666

67+
/**
68+
* The manager for {@link List} objects
69+
*/
70+
lists: ListManager;
71+
6772
/**
6873
* The class for working with sampled tweet stream
6974
*/
@@ -92,6 +97,7 @@ export class Client extends BaseClient {
9297
this.tweets = new TweetManager(this);
9398
this.users = new UserManager(this);
9499
this.spaces = new SpaceManager(this);
100+
this.lists = new ListManager(this);
95101
this.sampledTweets = new SampledTweetStream(this);
96102
this.filteredTweets = new FilteredTweetStream(this);
97103
}

src/errors/ErrorMessages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ const messages = {
1616
USER_CONTEXT_LOGIN_ERROR: (username: string) => `Could not fetch the user ${username}.`,
1717
CREDENTIALS_NOT_STRING: "One or more client credentials fields are missing or aren't of type string.",
1818
SPACE_RESOLVE_ID: (action: string) => `Could not resolve the space ID to ${action}.`,
19+
LIST_RESOLVE_ID: (action: string) => `Could not resolve the list ID to ${action}.`,
1920
};
2021

2122
for (const [key, message] of Object.entries(messages)) {

src/managers/ListManager.ts

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
import { BaseManager } from './BaseManager';
2+
import { List, RequestData } from '../structures';
3+
import { CustomError, CustomTypeError } from '../errors';
4+
import type { Client } from '../client';
5+
import type { CreateListOptions, ListResolvable, UpdateListOptions, UserResolvable } from '../typings';
6+
import type {
7+
DeleteListDeleteResponse,
8+
DeleteListRemoveMemberResponse,
9+
DeleteListUnfollowResponse,
10+
DeleteListUnpinResponse,
11+
PostListAddMemberJSONBody,
12+
PostListAddMemberResponse,
13+
PostListCreateJSONBody,
14+
PostListCreateResponse,
15+
PostListFollowJSONBody,
16+
PostListFollowResponse,
17+
PostListPinJSONBody,
18+
PostListPinResponse,
19+
PutListUpdateJSONBody,
20+
PutListUpdateResponse,
21+
Snowflake,
22+
} from 'twitter-types';
23+
24+
/**
25+
* The manager class that holds API methods for {@link List} objects and stores their cache
26+
*/
27+
export class ListManager extends BaseManager<Snowflake, ListResolvable, List> {
28+
/**
29+
* @param client The logged in {@link Client} instance
30+
*/
31+
constructor(client: Client) {
32+
super(client, List);
33+
}
34+
35+
/**
36+
* Creates a new list.
37+
* @param options The options for creating a list
38+
* @returns The created {@link List} object
39+
*/
40+
async create(options: CreateListOptions): Promise<List> {
41+
if (typeof options !== 'object') throw new CustomTypeError('INVALID_TYPE', 'options', 'object', true);
42+
const body: PostListCreateJSONBody = {
43+
name: options.name,
44+
description: options.description,
45+
private: options.private,
46+
};
47+
const requestData = new RequestData({ body, isUserContext: true });
48+
const res: PostListCreateResponse = await this.client._api.lists.post(requestData);
49+
const list = this.add(res.data.id, res.data);
50+
return list;
51+
}
52+
53+
/**
54+
* Deletes a list.
55+
* @param list The list to delete
56+
* @returns A boolean representing whether the specified list has been deleted
57+
*/
58+
async delete(list: ListResolvable): Promise<boolean> {
59+
const listId = this.resolveId(list);
60+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'delete');
61+
const requestData = new RequestData({ isUserContext: true });
62+
const res: DeleteListDeleteResponse = await this.client._api.lists(listId).delete(requestData);
63+
return res.data.deleted;
64+
}
65+
66+
/**
67+
* Updates a lists.
68+
* @param list The list to update
69+
* @param options The options for updating the list
70+
* @returns A boolean representing whether the specified list has been updated
71+
*/
72+
async update(list: ListResolvable, options: UpdateListOptions): Promise<boolean> {
73+
const listId = this.resolveId(list);
74+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'update');
75+
if (typeof options !== 'object') throw new CustomTypeError('INVALID_TYPE', 'options', 'object', true);
76+
const body: PutListUpdateJSONBody = {
77+
name: options.name,
78+
description: options.description,
79+
private: options.private,
80+
};
81+
const requestData = new RequestData({ body, isUserContext: true });
82+
const res: PutListUpdateResponse = await this.client._api.lists(listId).put(requestData);
83+
return res.data.updated;
84+
}
85+
86+
/**
87+
* Adds a member to a list
88+
* @param list The list to add the member to
89+
* @param member The user to add as a member of the list
90+
* @returns A boolean representing whether the specified user has been added to the List
91+
*/
92+
async addMember(list: ListResolvable, member: UserResolvable): Promise<boolean> {
93+
const listId = this.resolveId(list);
94+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'add member to');
95+
const userId = this.client.users.resolveId(member);
96+
if (!userId) throw new CustomError('USER_RESOLVE_ID', 'add to the list');
97+
const body: PostListAddMemberJSONBody = {
98+
user_id: userId,
99+
};
100+
const requestData = new RequestData({ body, isUserContext: true });
101+
const res: PostListAddMemberResponse = await this.client._api.lists(listId).members.post(requestData);
102+
return res.data.is_member;
103+
}
104+
105+
/**
106+
* Removes a member from a list.
107+
* @param list The list to remove the member from
108+
* @param member The member to remove from the list
109+
* @returns A boolean representing whether the specified user has been removed from the list
110+
*/
111+
async removeMember(list: ListResolvable, member: UserResolvable): Promise<boolean> {
112+
const listId = this.resolveId(list);
113+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'remove the member from');
114+
const userId = this.client.users.resolveId(member);
115+
if (!userId) throw new CustomError('USER_RESOLVE_ID', 'remove from the list');
116+
const requestData = new RequestData({ isUserContext: true });
117+
const res: DeleteListRemoveMemberResponse = await this.client._api
118+
.lists(listId)
119+
.members(userId)
120+
.delete(requestData);
121+
return !res.data.is_member;
122+
}
123+
124+
/**
125+
* Follows a list.
126+
* @param list The list to follow
127+
* @returns A boolean representing whether the authorized user followed the list
128+
*/
129+
async follow(list: ListResolvable): Promise<boolean> {
130+
const listId = this.resolveId(list);
131+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'follow');
132+
const loggedInUser = this.client.me;
133+
if (!loggedInUser) throw new CustomError('NO_LOGGED_IN_USER');
134+
const body: PostListFollowJSONBody = {
135+
list_id: listId,
136+
};
137+
const requestData = new RequestData({ body, isUserContext: true });
138+
const res: PostListFollowResponse = await this.client._api.users(loggedInUser.id).followed_lists.post(requestData);
139+
return res.data.following;
140+
}
141+
142+
/**
143+
* Unfollows a list.
144+
* @param list The list to unfollow
145+
* @returns A boolean representing whether the authorized user unfollowed the list
146+
*/
147+
async unfollow(list: ListResolvable): Promise<boolean> {
148+
const listId = this.resolveId(list);
149+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'unfollow');
150+
const loggedInUser = this.client.me;
151+
if (!loggedInUser) throw new CustomError('NO_LOGGED_IN_USER');
152+
const requestData = new RequestData({ isUserContext: true });
153+
const res: DeleteListUnfollowResponse = await this.client._api
154+
.users(loggedInUser.id)
155+
.followed_lists(listId)
156+
.delete(requestData);
157+
return !res.data.following;
158+
}
159+
160+
/**
161+
* Pins a list.
162+
* @param list The list to pin
163+
* @returns A boolean representing whether the authorized user pinned the list
164+
*/
165+
async pin(list: ListResolvable): Promise<boolean> {
166+
const listId = this.resolveId(list);
167+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'pin');
168+
const loggedInUser = this.client.me;
169+
if (!loggedInUser) throw new CustomError('NO_LOGGED_IN_USER');
170+
const body: PostListPinJSONBody = {
171+
list_id: listId,
172+
};
173+
const requestData = new RequestData({ body, isUserContext: true });
174+
const res: PostListPinResponse = await this.client._api.users(loggedInUser.id).pinned_lists.post(requestData);
175+
return res.data.pinned;
176+
}
177+
178+
/**
179+
* Unpins a list.
180+
* @param list The list to unpin
181+
* @returns A boolean representing whether the authorized user unpinned the list
182+
*/
183+
async unpin(list: ListResolvable): Promise<boolean> {
184+
const listId = this.resolveId(list);
185+
if (!listId) throw new CustomError('LIST_RESOLVE_ID', 'pin');
186+
const loggedInUser = this.client.me;
187+
if (!loggedInUser) throw new CustomError('NO_LOGGED_IN_USER');
188+
const requestData = new RequestData({ isUserContext: true });
189+
const res: DeleteListUnpinResponse = await this.client._api
190+
.users(loggedInUser.id)
191+
.pinned_lists(listId)
192+
.delete(requestData);
193+
return res.data.pinned;
194+
}
195+
}

src/managers/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export * from './BaseManager';
2+
export * from './ListManager';
23
export * from './SpaceManager';
34
export * from './TweetManager';
45
export * from './UserManager';

src/structures/List.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { BaseStructure } from './BaseStructure';
2+
import type { Client } from '../client';
3+
import type { APIList } from 'twitter-types';
4+
5+
export class List extends BaseStructure {
6+
/**
7+
* The name of the list
8+
*/
9+
name: string;
10+
11+
/**
12+
* The description of the list
13+
*/
14+
description: string | null;
15+
16+
/**
17+
* Whether the list is private
18+
*/
19+
private: boolean | null;
20+
21+
/**
22+
* @param client The logged in {@link Client} instance
23+
* @param data The raw data sent by the API for the list
24+
*/
25+
constructor(client: Client, data: APIList) {
26+
super(client, data);
27+
this.name = data.name;
28+
this.description = data.description ?? null;
29+
this.private = data.private ?? null;
30+
}
31+
}

src/structures/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export * from './misc';
22
export * from './BaseStructure';
33
export * from './ClientUser';
44
export * from './FilteredTweetStreamRule';
5+
export * from './List';
56
export * from './Media';
67
export * from './Place';
78
export * from './Poll';

src/typings/Interfaces.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -545,3 +545,28 @@ export interface CountTweetsOptions {
545545
export interface BaseStructureData {
546546
id: Snowflake;
547547
}
548+
549+
/**
550+
* The options used for creating a new list
551+
*/
552+
export interface CreateListOptions {
553+
/**
554+
* The name of the list
555+
*/
556+
name: string;
557+
558+
/**
559+
* The description of the list
560+
*/
561+
description?: string;
562+
563+
/**
564+
* Whether the list should be private
565+
*/
566+
private?: boolean;
567+
}
568+
569+
/**
570+
* The options used to update a list
571+
*/
572+
export type UpdateListOptions = Partial<CreateListOptions>;

0 commit comments

Comments
 (0)