Skip to content

Commit

Permalink
Merge pull request #16 from daneden/feat-oauth2
Browse files Browse the repository at this point in the history
Add OAuth 2.0 user authentication
  • Loading branch information
daneden authored Apr 2, 2022
2 parents a3dc5b1 + da4c0b2 commit 17b65be
Show file tree
Hide file tree
Showing 10 changed files with 309 additions and 82 deletions.
5 changes: 4 additions & 1 deletion Demo App/Twift_SwiftUI/Tweets/PostTweet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,15 @@ struct PostTweet: View {

AsyncButton {
do {
let media = MutableMedia(mediaIds: [mediaKey])
let media = mediaKey.isEmpty ? nil : MutableMedia(mediaIds: [mediaKey])
let tweet = MutableTweet(text: text, media: media)

let response = try await twitterClient.postTweet(tweet)

tweetId = response.data.id

text = ""
mediaKey = ""
} catch {
print(error)
}
Expand Down
31 changes: 6 additions & 25 deletions Demo App/Twift_SwiftUI/Tweets/UserLikes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Twift

struct UserLikes: View {
@EnvironmentObject var twitterClient: Twift
@State var tweets: [Tweet]?
@State var tweets: [Tweet] = []
@State var errors: [TwitterAPIError] = []
@State var meta: Meta?
@State var includes: Tweet.Includes?
Expand All @@ -24,27 +24,7 @@ struct UserLikes: View {
.keyboardType(.numberPad)

AsyncButton(action: {
do {
let result = try await twitterClient.getLikedTweets(
for: userId,
fields: Set(Tweet.publicFields),
expansions: [.authorId(userFields: [\.profileImageUrl])]
)

withAnimation {
tweets = result.data
includes = result.includes
errors = result.errors ?? []
}
} catch {
if let error = error as? TwitterAPIError {
withAnimation { errors = [error] }
} else if let error = (error as? TwitterAPIManyErrors)?.errors {
withAnimation { errors = error }
} else {
print(error.localizedDescription)
}
}
await getPage()
}) {
Text("Get user likes")
}
Expand All @@ -59,13 +39,14 @@ struct UserLikes: View {
}.navigationTitle("Get User Likes")
}

func getPage(_ token: String?) async {
func getPage(_ token: String? = nil) async {
do {
let result = try await twitterClient.getLikedTweets(
for: userId,
fields: Set(Tweet.publicFields),
expansions: [.authorId(userFields: [\.profileImageUrl])],
paginationToken: token
paginationToken: token,
maxResults: 100
)

withAnimation {
Expand All @@ -78,7 +59,7 @@ struct UserLikes: View {
if let error = error as? TwitterAPIError {
withAnimation { errors = [error] }
} else {
print(error.localizedDescription)
print(error)
}
}
}
Expand Down
25 changes: 12 additions & 13 deletions Demo App/Twift_SwiftUI/Twift_SwiftUIApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extension Twift {
switch authenticationType {
case .appOnly(_): return false
case .userAccessTokens(_, _): return true
case .oauth2UserAuth(_): return true
}
}
}
Expand Down Expand Up @@ -40,20 +41,18 @@ struct Twift_SwiftUIApp: App {
NavigationView {
Form {
Section(
header: Text("User Access Tokens"),
footer: Text("Use this authentication method for most cases.")
header: Text("OAuth 2.0 User Authentication"),
footer: Text("Use this authentication method for most cases. This test app enables all user scopes by default.")
) {
Button {
Twift.Authentication().requestUserCredentials(clientCredentials: clientCredentials, callbackURL: URL(string: TWITTER_CALLBACK_URL)!) { (userCredentials, error) in
if let error = error {
print(error.localizedDescription)
}
AsyncButton {
let (user, _) = await Twift.Authentication().authenticateUser(clientId: "Sm5PSUhRNW9EZ3NXb0tJQkI5WU06MTpjaQ",
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
scope: Set(OAuth2Scope.allCases))

if let user = user {
container.client = Twift(.oauth2UserAuth(user))

if let creds = userCredentials {
DispatchQueue.main.async {
container.client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: creds))
}
}
try? await container.client?.refreshOAuth2AccessToken()
}
} label: {
Text("Sign In With Twitter")
Expand All @@ -62,7 +61,7 @@ struct Twift_SwiftUIApp: App {

Section(
header: Text("App-Only Bearer Token"),
footer: Text("Use this authentication method for app-only methods such as filtered streams")
footer: Text("Use this authentication method for app-only methods such as filtered streams.")
) {
TextField("Enter Bearer Token", text: $bearerToken)
Button {
Expand Down
52 changes: 13 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,35 @@
Twift is an asynchronous Swift library for the Twitter v2 API.

- [x] No external dependencies
- [x] Only one callback-based method ([`Authentication.requestUserCredentials`](https://github.com/daneden/Twift/wiki/Twift_Authentication#requestusercredentialsclientcredentialscallbackurlpresentationcontextproviderwith))
- [x] Fully async
- [x] Full Swift type definitions/wrappers around Twitter's API objects

## Quick Start Guide

New `Twift` instances must be initiated with either User Access Tokens or an App-Only Bearer Token:
New `Twift` instances must be initiated with either OAuth 2.0 user authentication or an App-Only Bearer Token:

```swift
// User access tokens
let clientCredentials = OAuthCredential(key: CONSUMER_KEY, secret: CONSUMER_SECRET)
let userCredentials = OAuthCredential(key: ACCESS_KEY, secret: ACCESS_SECRET)
let userAuthenticatedClient = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: userCredentials)
let oauthUser: OAuth2User = OAUTH2_USER
let userAuthenticatedClient = Twift(.oauth2UserAuth(oauthUser: oauthUser)

// Bearer token
let appOnlyClient = Twift(.appOnly(bearerToken: BEARER_TOKEN)
```

You can acquire user access tokens by authenticating the user with `Twift.Authentication().requestUserCredentials()`:
You can authenticating users with `Twift.Authentication().authenticateUser()`:

```swift
var client: Twift?

Twift.Authentication().requestUserCredentials(
clientCredentials: clientCredentials,
callbackURL: URL(string: "twift-test://")!
) { (userCredentials, error) in
if let creds = userCredentials {
client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: creds))
}
let (oauthUser, error) = await Twift.Authentication().authenticateUser(
clientId: TWITTER_CLIENT_ID,
redirectUri: URL(string: TWITTER_CALLBACK_URL)!,
scope: Set(OAuth2Scope.allCases)
)

if let oauthUser = oauthUser {
client = Twift(.oauth2UserAuth(oauthUser))
}
```

Expand Down Expand Up @@ -125,29 +125,3 @@ let me = response?.data
// The user's pinned Tweet
let tweet = response?.includes?.tweets.first
```

### Optional Actor IDs

Many of Twift's methods require a `User.ID` in order to make requests on behalf of that user. For convenience, this parameter is often marked as optional, since the currently-authenticated `User.ID` may be found on the instance's authentication type:

```swift
var client: Twift?
var credentials: OAuthCredential?

Twift.Authentication().requestUserCredentials(
clientCredentials: clientCredentials,
callbackURL: URL(string: "twift-test://")!
) { (userCredentials, error) in
if let userCredentials = userCredentials {
client = Twift(.userAccessTokens(clientCredentials: clientCredentials, userCredentials: userCredentials))
credentials = userCredentials
}
}

// Elsewhere in the app...

// These two calls are equivalent since the client was initialized with an OAuthCredential containing the authenticated user's ID
let result1 = try? await client?.followUser(sourceUserId: credentials.userId!, targetUserId: "12")
let result2 = try? await client?.followUser(targetUserId: "12")

```
10 changes: 9 additions & 1 deletion Sources/Twift+API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ extension Twift {
body: Data? = nil,
expectedReturnType: T.Type
) async throws -> T {
if case AuthenticationType.oauth2UserAuth(_) = self.authenticationType {
try await self.refreshOAuth2AccessToken()
}

let url = getURL(for: route, queryItems: queryItems)
var request = URLRequest(url: url)

Expand All @@ -17,7 +21,7 @@ extension Twift {
}

signURLRequest(method: method, body: body, request: &request)

let (data, _) = try await URLSession.shared.data(for: request)

return try decodeOrThrow(decodingType: T.self, data: data)
Expand Down Expand Up @@ -70,7 +74,11 @@ extension Twift {
consumerCredentials: clientCredentials,
userCredentials: userCredentials
)
case .oauth2UserAuth(let oauthUser):
request.addValue("Bearer \(oauthUser.accessToken)", forHTTPHeaderField: "Authorization")
}

request.httpMethod = method.rawValue
}
}

Expand Down
Loading

2 comments on commit 17b65be

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View documentation coverage after this change
Filename Coverage
Total 87.53%
Twift+API.swift 100.00%
Twift+Authentication.swift 94.87%
Twift+Blocks.swift 100.00%
Twift+Errors.swift 93.75%
Twift+Follows.swift 100.00%
Twift+Likes.swift 100.00%
Twift+Lists.swift 100.00%
Twift+Media.swift 92.59%
Twift+Mutes.swift 100.00%
Twift+Retweets.swift 100.00%
Twift+Search.swift 100.00%
Twift+Spaces.swift 90.00%
Twift+Streams.swift 100.00%
Twift+Tweets.swift 84.21%
Twift+Users.swift 100.00%
Twift.swift 100.00%
Types+List.swift 93.33%
Types+Media.swift 82.76%
Types+Place.swift 76.67%
Types+Poll.swift 86.67%
Types+Spaces.swift 65.71%
Types+Stream.swift 75.00%
Types+Tweet.swift 83.12%
Types+User.swift 88.37%
Types.swift 100.00%

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View documentation coverage after this change
Filename Coverage
Total 87.53%
Twift+API.swift 100.00%
Twift+Authentication.swift 94.87%
Twift+Blocks.swift 100.00%
Twift+Errors.swift 93.75%
Twift+Follows.swift 100.00%
Twift+Likes.swift 100.00%
Twift+Lists.swift 100.00%
Twift+Media.swift 92.59%
Twift+Mutes.swift 100.00%
Twift+Retweets.swift 100.00%
Twift+Search.swift 100.00%
Twift+Spaces.swift 90.00%
Twift+Streams.swift 100.00%
Twift+Tweets.swift 84.21%
Twift+Users.swift 100.00%
Twift.swift 100.00%
Types+List.swift 93.33%
Types+Media.swift 82.76%
Types+Place.swift 76.67%
Types+Poll.swift 86.67%
Types+Spaces.swift 65.71%
Types+Stream.swift 75.00%
Types+Tweet.swift 83.12%
Types+User.swift 88.37%
Types.swift 100.00%

Please sign in to comment.