-
Notifications
You must be signed in to change notification settings - Fork 0
/
TwitterRequest.swift
256 lines (221 loc) · 9.77 KB
/
TwitterRequest.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
//
// TwitterRequest.swift
// Twitter
//
// Created by CS193p Instructor.
// Copyright (c) 2015 Stanford University. All rights reserved.
//
import Foundation
import Accounts
import Social
import CoreLocation
// Simple Twitter query class
// Create an instance of it using one of the initializers
// Set the requestType and parameters (if not using a convenience init that sets those)
// Call fetch (or fetchTweets if fetching Tweets)
// The handler passed in will be called when the information comes back from Twitter
// Once a successful fetch has happened,
// a follow-on TwitterRequest to get more Tweets (newer or older) can be created
// using the requestFor{Newer,Older} methods
private var twitterAccount: ACAccount?
public class TwitterRequest
{
public let requestType: String
public let parameters: [String:String]
// designated initializer
public init(_ requestType: String, _ parameters: Dictionary<String, String> = [:]) {
self.requestType = requestType
self.parameters = parameters
}
// convenience initializer for creating a TwitterRequest that is a search for Tweets
public convenience init(search: String, count: Int = 0, _ resultType: SearchResultType = .Mixed, _ region: CLCircularRegion? = nil) {
var parameters = [TwitterKey.Query : search]
if count > 0 {
parameters[TwitterKey.Count] = "\(count)"
}
switch resultType {
case .Recent: parameters[TwitterKey.ResultType] = TwitterKey.ResultTypeRecent
case .Popular: parameters[TwitterKey.ResultType] = TwitterKey.ResultTypePopular
default: break
}
if let geocode = region {
parameters[TwitterKey.Geocode] = "\(geocode.center.latitude),\(geocode.center.longitude),\(geocode.radius/1000.0)km"
}
self.init(TwitterKey.SearchForTweets, parameters)
}
public enum SearchResultType {
case Mixed
case Recent
case Popular
}
// convenience "fetch" for when self is a request that returns Tweet(s)
// handler is not necessarily invoked on the main queue
public func fetchTweets(handler: ([Tweet]) -> Void) {
fetch { results in
var tweets = [Tweet]()
var tweetArray: NSArray?
if let dictionary = results as? NSDictionary {
if let tweets = dictionary[TwitterKey.Tweets] as? NSArray {
tweetArray = tweets
} else if let tweet = Tweet(data: dictionary) {
tweets = [tweet]
}
} else if let array = results as? NSArray {
tweetArray = array
}
if tweetArray != nil {
for tweetData in tweetArray! {
if let tweet = Tweet(data: tweetData as? NSDictionary) {
tweets.append(tweet)
}
}
}
handler(tweets)
}
}
public typealias PropertyList = AnyObject
// send an arbitrary request off to Twitter
// calls the handler (not necessarily on the main queue)
// with the JSON results converted to a Property List
public func fetch(handler: (results: PropertyList?) -> Void) {
performTwitterRequest(SLRequestMethod.GET, handler: handler)
}
// generates a request for older Tweets than were returned by self
// only makes sense if self has done a fetch already
// only makes sense for requests for Tweets
public var requestForOlder: TwitterRequest? {
return min_id != nil ? modifiedRequest(parametersToChange: [TwitterKey.MaxID : min_id!]) : nil
}
// generates a request for newer Tweets than were returned by self
// only makes sense if self has done a fetch already
// only makes sense for requests for Tweets
public var requestForNewer: TwitterRequest? {
return (max_id != nil) ? modifiedRequest(parametersToChange: [TwitterKey.SinceID : max_id!], clearCount: true) : nil
}
// MARK: - Private Implementation
// creates an appropriate SLRequest using the specified SLRequestMethod
// then calls the other version of this method that takes an SLRequest
// handler is not necessarily called on the main queue
func performTwitterRequest(method: SLRequestMethod, handler: (PropertyList?) -> Void) {
let jsonExtension = (self.requestType.rangeOfString(JSONExtension) == nil) ? JSONExtension : ""
let request = SLRequest(
forServiceType: SLServiceTypeTwitter,
requestMethod: method,
URL: NSURL(string: "\(TwitterURLPrefix)\(self.requestType)\(jsonExtension)"),
parameters: self.parameters
)
performTwitterRequest(request, handler: handler)
}
// sends the request to Twitter
// unpackages the JSON response into a Property List
// and calls handler (not necessarily on the main queue)
func performTwitterRequest(request: SLRequest, handler: (PropertyList?) -> Void) {
if let account = twitterAccount {
request.account = account
request.performRequestWithHandler { (jsonResponse, httpResponse, _) in
var propertyListResponse: PropertyList?
if jsonResponse != nil {
do {
propertyListResponse = try NSJSONSerialization.JSONObjectWithData(
jsonResponse,
options: NSJSONReadingOptions.MutableLeaves
)
if propertyListResponse == nil {
let error = "Couldn't parse JSON response."
self.log(error)
propertyListResponse = error
}
}
catch {
}
} else {
let error = "No response from Twitter."
self.log(error)
propertyListResponse = error
}
self.synchronize {
self.captureFollowonRequestInfo(propertyListResponse)
}
handler(propertyListResponse)
}
} else {
let accountStore = ACAccountStore()
let twitterAccountType = accountStore.accountTypeWithAccountTypeIdentifier(ACAccountTypeIdentifierTwitter)
accountStore.requestAccessToAccountsWithType(twitterAccountType, options: nil) { (granted, _) in
if granted {
if let account = accountStore.accountsWithAccountType(twitterAccountType)?.last as? ACAccount {
twitterAccount = account
self.performTwitterRequest(request, handler: handler)
} else {
let error = "Couldn't discover Twitter account type."
self.log(error)
handler(error)
}
} else {
let error = "Access to Twitter was not granted."
self.log(error)
handler(error)
}
}
}
}
private var min_id: String? = nil
private var max_id: String? = nil
// modifies parameters in an existing request to create a new one
private func modifiedRequest(parametersToChange parametersToChange: Dictionary<String,String>, clearCount: Bool = false) -> TwitterRequest {
var newParameters = parameters
for (key, value) in parametersToChange {
newParameters[key] = value
}
if clearCount { newParameters[TwitterKey.Count] = nil }
return TwitterRequest(requestType, newParameters)
}
// captures the min_id and max_id information
// to support requestForNewer and requestForOlder
private func captureFollowonRequestInfo(propertyListResponse: PropertyList?) {
if let responseDictionary = propertyListResponse as? NSDictionary {
self.max_id = responseDictionary.valueForKeyPath(TwitterKey.SearchMetadata.MaxID) as? String
if let next_results = responseDictionary.valueForKeyPath(TwitterKey.SearchMetadata.NextResults) as? String {
for queryTerm in next_results.componentsSeparatedByString(TwitterKey.SearchMetadata.Separator) {
if queryTerm.hasPrefix("?\(TwitterKey.MaxID)=") {
let next_id = queryTerm.componentsSeparatedByString("=")
if next_id.count == 2 {
self.min_id = next_id[1]
}
}
}
}
}
}
// debug println with identifying prefix
private func log(whatToLog: AnyObject) {
debugPrint("TwitterRequest: \(whatToLog)")
}
// synchronizes access to self across multiple threads
private func synchronize(closure: () -> Void) {
objc_sync_enter(self)
closure()
objc_sync_exit(self)
}
// constants
let JSONExtension = ".json"
let TwitterURLPrefix = "https://api.twitter.com/1.1/"
// keys in Twitter responses/queries
struct TwitterKey {
static let Count = "count"
static let Query = "q"
static let Tweets = "statuses"
static let ResultType = "result_type"
static let ResultTypeRecent = "recent"
static let ResultTypePopular = "popular"
static let Geocode = "geocode"
static let SearchForTweets = "search/tweets"
static let MaxID = "max_id"
static let SinceID = "since_id"
struct SearchMetadata {
static let MaxID = "search_metadata.max_id_str"
static let NextResults = "search_metadata.next_results"
static let Separator = "&"
}
}
}