diff --git a/api_v2/data_interface/tweet.ts b/api_v2/data_interface/tweet.ts index 3bbef9e..e7c29cc 100644 --- a/api_v2/data_interface/tweet.ts +++ b/api_v2/data_interface/tweet.ts @@ -71,3 +71,25 @@ export interface IncludesObject { media?: MediaObject[]; polls?: PollObject; } + +export interface SelectTweetFields { + "attachments"?: boolean; + "author_id"?: boolean; + "context_annotations"?: boolean; + "conversation_id"?: boolean; + "created_at"?: boolean; + "entities"?: boolean; + "geo"?: boolean; + "id"?: boolean; + "in_reply_to_user_id"?: boolean; + "lang"?: boolean; + "non_public_metrics"?: boolean; + "public_metrics"?: boolean; + "organic_metrics"?: boolean; + "promoted_metrics"?: boolean; + "possibly_sensitive"?: boolean; + "referenced_tweets"?: boolean; + "source"?: boolean; + "text"?: boolean; + "withheld"?: boolean; +} diff --git a/api_v2/data_interface/user.ts b/api_v2/data_interface/user.ts index 088a8b8..8a59830 100644 --- a/api_v2/data_interface/user.ts +++ b/api_v2/data_interface/user.ts @@ -10,7 +10,7 @@ export interface UserObject { entities?: Entities; location?: string; pinned_tweet_id?: string; - profile_image_url: string; + profile_image_url?: string; protected?: boolean; public_metrics?: { followers_count: number; @@ -22,3 +22,21 @@ export interface UserObject { verified?: boolean; withheld?: WithHeld; } + +export interface SelectUserFields { + "created_at"?: boolean; + "description"?: boolean; + "entities"?: boolean; + "id"?: boolean; + "location"?: boolean; + "name"?: boolean; + "pinned_tweet_id"?: boolean; + "profile_image_url"?: boolean; + "protected"?: boolean; + "public_metrics"?: boolean; + "url"?: boolean; + "username"?: boolean; + "verified"?: boolean; + "verified_type"?: boolean; + "withheld"?: boolean; +} diff --git a/api_v2/users/lookup.ts b/api_v2/users/lookup.ts new file mode 100644 index 0000000..05ce4e2 --- /dev/null +++ b/api_v2/users/lookup.ts @@ -0,0 +1,41 @@ +import { addParamOption, endpoints, getUrl } from "../../util.ts"; + +import { SelectTweetFields, TweetObject } from "../data_interface/tweet.ts"; +import { SelectUserFields, UserObject } from "../data_interface/user.ts"; + +interface UsersMeParam { + "expansions"?: { + "pinned_tweet_id"?: boolean; + }; + "tweet.fields"?: SelectTweetFields; + "user.fields"?: SelectUserFields; +} + +interface UsersMeResponse { + data: UserObject; + includes?: { + tweets?: TweetObject[]; + }; +} + +/** + * Returns information about an authorized user. + * + * https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me + */ +export async function getUsersMe(auth: string, param: UsersMeParam) { + const url = getUrl(endpoints.api_v2.users.me); + //url.searchParams.set("ids", param.ids); + addParamOption(url, param); + console.log(url); + const res = await (await fetch( + url.toString(), + { + headers: new Headers({ + "Authorization": `Bearer ${auth}`, + }), + }, + )).json(); + + return res as UsersMeResponse; +} diff --git a/auth/oauth.ts b/auth/oauth.ts new file mode 100644 index 0000000..741edb6 --- /dev/null +++ b/auth/oauth.ts @@ -0,0 +1,124 @@ +import { oAuth1Fetch, OAuth1Info } from "../deps.ts"; + +import { endpoints, getUrl } from "../util.ts"; + +type RequestTokenParam = { + oauth_callback: string; + x_auth_access_type?: "read" | "write"; +}; +type AuthenticationParam = { + oauth_token: string; + force_login?: boolean; + screen_name?: string; +}; +type AuthorizeParam = AuthenticationParam; +type AccessTokenParam = { + oauth_token: string; + oauth_verifier: string; +}; + +/** + * **Step 1** of the 3-legged OAuth flow and Sign in with Twitter + * + * Allows a Consumer application to obtain an OAuth Request Token to request user authorization. + * + * https://developer.twitter.com/en/docs/authentication/api-reference/request_token + */ +export async function requestOAuthToken( + auth: OAuth1Info, + param: RequestTokenParam, +) { + const url = getUrl(endpoints.oauth.requestToken); + url.searchParams.set("oauth_callback", param.oauth_callback); + if (param.x_auth_access_type) { + url.searchParams.set("x_auth_access_type", param.x_auth_access_type); + } + const res = await oAuth1Fetch(auth, url); + if (res.ok === false) { + throw new Error("Failed to request OAuth token"); + } + + const bodyText = await res.text(); + const bodySp = new URLSearchParams(bodyText); + const oauth_token = bodySp.get("oauth_token") as string; + const oauth_token_secret = bodySp.get("oauth_token_secret") as string; + const oauth_callback_confirmed = + bodySp.get("oauth_callback_confirmed") === "true" ? true : false; + + return { oauth_token, oauth_token_secret, oauth_callback_confirmed }; +} + +/** + * **Step 2** of the 3-legged OAuth flow and Sign in with Twitter + * + * Allows a Consumer application to use an OAuth Request Tokento request user authorization. + * + * https://developer.twitter.com/en/docs/authentication/api-reference/authenticate + */ +export function getAuthenticateUrl(param: AuthenticationParam) { + const url = getUrl(endpoints.oauth.authenticate); + url.searchParams.set("oauth_token", param.oauth_token); + if (param.force_login !== undefined) { + url.searchParams.set("force_login", param.force_login.toString()); + } + if (param.screen_name) { + url.searchParams.set("screen_name", param.screen_name); + } + + return url; +} + +/** + * **Step 2** of the 3-legged OAuth flow and Sign in with Twitter + * + * Allows a Consumer application to use an OAuth Request Tokento request user authorization. + * + * https://developer.twitter.com/en/docs/authentication/api-reference/authorize + */ +export function getAuthorizeUrl(param: AuthorizeParam) { + const url = getUrl(endpoints.oauth.authorize); + url.searchParams.set("oauth_token", param.oauth_token); + if (param.force_login !== undefined) { + url.searchParams.set("force_login", param.force_login.toString()); + } + if (param.screen_name) { + url.searchParams.set("screen_name", param.screen_name); + } + + return url; +} + +/** + * **Step 3** of the 3-legged OAuth flow and Sign in with Twitter + * + * Allows a Consumer application to exchange the OAuth Request Token for an OAuth Access Token. + * + * https://developer.twitter.com/en/docs/authentication/api-reference/access_token + */ +export async function getOAuthAccessToken(param: AccessTokenParam) { + const url = getUrl(endpoints.oauth.accessToken); + url.searchParams.set("oauth_token", param.oauth_token); + url.searchParams.set("oauth_verifier", param.oauth_verifier); + + const res = await fetch(url); + const a = new URLSearchParams(await res.text()); + const oauth_token = a.get("oauth_token") as string; + const oauth_token_secret = a.get("oauth_token_secret") as string; + const user_id = a.get("user_id") as string; + const screen_name = a.get("screen_name") as string; + + return { + oauth_token, + oauth_token_secret, + user_id, + screen_name, + }; +} + +export async function invalidateOAuthToken(auth: OAuth1Info) { + const url = getUrl(endpoints.oauth.invalidateToken); + const res = await oAuth1Fetch(auth, url); + if (res.ok === false) { + throw new Error("invalidateToken failed"); + } +} diff --git a/deps-test.ts b/deps-test.ts index 9015754..a3106fd 100644 --- a/deps-test.ts +++ b/deps-test.ts @@ -1 +1 @@ -export * from "https://deno.land/std@0.165.0/testing/asserts.ts"; +export * from "https://deno.land/std@0.203.0/testing/asserts.ts"; diff --git a/mod.ts b/mod.ts index 831a701..a6a9fa5 100644 --- a/mod.ts +++ b/mod.ts @@ -7,6 +7,8 @@ export * from "./api_v1/tweets/search.ts"; export * from "./api_v2/tweets/filtered_stream.ts"; export * from "./api_v2/tweets/lookup.ts"; +export * from "./api_v2/users/lookup.ts"; +export * from "./auth/oauth.ts"; export * from "./auth/oauth2.ts"; export * from "./util.ts"; diff --git a/util.ts b/util.ts index 1415fbb..a141dbc 100644 --- a/util.ts +++ b/util.ts @@ -5,6 +5,13 @@ export const setHost = (url: string) => { }; export const endpoints = { + oauth: { + requestToken: "/oauth/request_token", + authenticate: "/oauth/authenticate", + authorize: "/oauth/authorize", + accessToken: "/oauth/access_token", + invalidateToken: "/oauth/invalidate_token", + }, outh2: { getToken: "/oauth2/token", }, @@ -32,6 +39,9 @@ export const endpoints = { rules: "/2/tweets/search/stream/rules", connect: "/2/tweets/search/stream", }, + users: { + me: "/2/users/me", + }, }, };