Queenfisher - Cross-Platform Google APIs for Swift built with NIO
What's Done:
- Authenticating using OAuth & using refresh tokens to continually fetch new access tokens
- Authenticating using a service account
- GMail -- reading, modifying, fetching, sending & replying to emails
- Spreadsheets -- reading, modifying & writing to sheets
- Synchronize & maintain a database on Sheets
Installing
- Queenfisher is written in Swift 5.2, so you need either XCode 11.4 or Swift 5.2 installed on your system.
- Add Queenfisher to your swift package:
...
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/adiwajshing/Queenfisher.git", from: "0.1.0")
],
targets: [
.target(name: "MyTarget", dependencies: ["Queenfisher", ...])
]
...
- Finally, import Queenfisher in your code using:
import Queenfisher
Authenticating with Google
- Before you can use these APIs, you need to have a project setup on Google Cloud Platform, you can create one here.
- Once you have a project setup, you must enable the APIs you want to use. Queenfisher currently wraps around the GMail & Sheets API, so you can enable either or both.
- To authenticate using O-Auth
- Create & download your client secret, learn how to do that here.
- Store the downloaded JSON somewhere nice & safe.
- Now you can load the JSON & generate an access token:
import Queenfisher import NIO let pathToSecret = URL(fileURLWithPath: "Path/to/client_secret.json") let pathToToken = URL(fileURLWithPath: "Path/to/my_token.json") // place to save the generated token let client: GoogleOAuthClient = try .loading(from: pathToSecret) // generate the authentication url where you can sign in & get your access token let authUrl = client.authUrl(for: .mailAll + .sheets) // authenticate for full access to mail & spreadsheets print ("sign in here & paste the code from the link below: \(authUrl)") // open the url in a browser /* Once you sign off on the permissions, google will redirect you to the url you specified in the client secret If you don't have a server listening, you can just extract the code & paste it here, and you will get your access & refresh tokens The code will be in the url query like: http://localhost:8080?code=abcdefg&scope=blahblah Paste `abcdefg` below */ let code = readLine(strippingNewline: true)! let accessToken = try client.fetchToken(fromCode: code).wait() // will exchange code for access & refresh tokens print("got access token: \($0)") /* You can now use this access token for sheets or gmail */ try JSONEncoder().encode(accessToken).write(to: pathToToken) // save the token as a JSON
- To continually ensure you have an active token, you can create a factory. New tokens are fetched using the refresh token whenever one expires. Do note, that refresh tokens never expire, they stop working whenever the user revokes access to your GCP project.
// get your client secret let client: GoogleOAuthClient = try .loading(from: pathToSecret) // create an authentication factory using the access token & secret let authFactory = try client.factory(usingAccessToken: .loading(fromJSONAt: pathToToken)) /* Use authFactory as your access mediator when accessing APIs. This will ensure you always have an active access token */
- To authenticate using a Service Account:
- Create a service account or use one you already have, learn about creating one here.
- Download the credentials of said service account.
import Queenfisher let pathToAcc = URL(fileURLWithPath: "Path/to/service_account.json") let serviceAcc: GoogleServiceAccount = try .loading(fromJSONAt: pathToAcc) let authFactory = serviceAcc.factory (forScope: .sheets) // get authentication for sheets /* Use authFactory as your access mediator when accessing APIs. This will ensure you always have an active access token */
GMail API
- Create an instance
import Queenfisher import Promises // create an authentication factory using the access token & secret // make sure your token has access to GMail // do note, service accounts cannot access GMail unless with GSuite accounts let client: GoogleOAuthClient = try .loading(from: pathToSecret) let authFactory = try client.factory(usingAccessToken: .loading(fromJSONAt: pathToToken)) let gmail: GMail = .init(using: authFactory) let profile = try gmail.profile().wait() print ("Oh hello: \(profile.emailAddress)") // print email address
- Listing emails
You can refine your search by specifying query parameters mentioned here. For example:
gmail.list() // lists all messages in inbox, sent & drafts ordered by timestamp .map { print ("got \($0.resultSizeEstimate) messages") if let messages = $0.messages { for m in messages { // metadata of messages print ("id: \(m.id)") } } }
gmail.list(q: "is:unread") // lists all unread messages gmail.list(q: "subject:permission") // subject contains the word `permission` gmail.list(q: "from:xyz@yahoo.com") // all emails from this email address
- Reading emails
Dive deeper into the GMail.Message class to get the attachements & the entire text of the email.
gmail.list() // lists all messages in inbox, sent & drafts ordered by timestamp .flatMap { gmail.get(id: $0.messages![0].id, format: .full) } // get the first email received .map { print ("email from: \($0.from!)") print ("email subject: \($0.subject!)") print ("email snippet: \($0.snippet!)") }
- Sending emails
The
let attachFile = URL(fileURLWithPath: "Path/to/fave_image.jpeg") let mail: GMail.Message = .init(to: [ .namedEmail("Myself & I", profile.emailAddress) ], subject: "Hello", text: "My name <b>Jeff</b>.", attachments: [ try! .attachment(fileAt: attachFile) ]) gmail.send (message: mail) .whenComplete { print ("yay sent mail with ID: \($0.id)") } .whenFailure { print ("error in sending: \($0)") }
text
in emails must be some html text. - Replying to emails
let profile = try await(gmail.profile()) // get profile gmail.list() .flatMap { gmail.get(id: $0.messages![0].id, format: .full) } // get the first email received .flatMap { message -> EventLoopFuture<GMail.Message> in let isMailFromMe = $0.from!.email == profile.emailAddress // determine if the email was sent by me let reply: GMail.Message = GMail.Message(replyingTo: message, fromMe: isMailFromMe, text: "Wow this is a reply")! return gmail.send (message: reply) } .whenComplete { print ("yay sent reply with ID: \($0.id)") }
- Fetching Emails
// fetch unread emails every 60 seconds // note: once a mail is forwarded to this handler, it will not be forwarded again in the future gmail.fetch(over: .seconds(60), q: "is:unread") { result in switch result { case .success(let messages): print ("got \(messages.count) new messages") break case .failure(let error): print("Oh no, got an error: \(error)") break } }
- Misc Tasks
gmail.markRead (id: idOfTheMessage) .whenComplete { print ("yay read mail with ID: \($0.id)") }
gmail.trash (id: idOfTheMessage) .whenComplete { print ("yay trashed mail with ID: \($0.id)") }
gmail.modify (id: idOfTheMessage, adddingLabelIds: ["UNREAD"]) // effectively mark an email as unread .whenComplete { print ("yay modified mail with ID: \($0.id)") }
Sheets API
- Getting a Spreadsheet:
import Queenfisher import NIO // create an authentication factory using the access token & secret // make sure your token has access to GMail // do note, service accounts cannot access GMail unless with GSuite accounts let client: GoogleOAuthClient = try .loading(from: pathToSecret) let authFactory = try client.factory(usingAccessToken: .loading(fromJSONAt: pathToToken)) let spreadsheetId = "abcdefghi" // insert actual spreadsheet ID let spreadsheet: Spreadsheet = try .get(spreadsheetId, using: authFactory).wait () print("Got spreadsheet '\(spreadsheet.properties.title)', sheets: \(spreadsheet.sheets.map({$0.properties.title}))")
- Writing rows to a spreadsheet:
// get the sheet ID, it's the unique ID for every sheet, you'll need it for almost all operations let sheetId = spreadsheet.sheet (forTitle: "Sheet 1")!.properties.sheetId! let rows = [ ["hello", "this", "is", "jeff"], ["yes", "my", "name", "jeff"], ["of course", "this", "is", "jeff"] ] // write these rows to the start of the spreadsheet spreadsheet.writeRows (sheetId: sheetId, rows: rows, starting: .cell(0,0)) .whenComplete { _ in print ("yay done") }
- Appending rows to a spreadsheet:
// get the sheet ID, it's the unique ID for every sheet, you'll need it for almost all operations let sheetId = spreadsheet.sheet (forTitle: "Sheet 1")!.properties.sheetId! let rows = [ ["wow", "more", "rows", "!"], ["yes", "this", "is", "great"] ] // append these rows after the last row with data in the sheet spreadsheet.appendRows (sheetId: sheetId, rows: rows) .whenComplete { _ in print ("yay done") }
- Reading from a spreadsheet:
let sheetId = spreadsheet.sheet (forTitle: "Sheet 1")!.properties.sheetId! spreadsheet.read (sheetId: sheetId) .whenComplete { print ("\($0.values)") } /* or if you want to read a specific range */ spreadsheet.read (sheetId: sheetId, range: (.row(1), .row(5))) // read all columns in row index 1 to 5 .whenComplete { print ("\($0.values)") }
- Inserting empty rows/columns into a sheet:
spreadsheet.insert(sheetId: sheetId, range: 2..<4, dimension: .columns) // insert 2 columns at index 2 .whenComplete { _ in print ("yay inserted") }
- Appending empty rows/columns into a sheet:
spreadsheet.append(sheetId: sheetId, size: 3, dimension: .columns) // append 3 columns .whenComplete { _ in print ("yay appended") }
- Moving rows/columns in a sheet:
spreadsheet.move(sheetId: sheetId, range: 2..<3, to: 2, to: 1, dimension: .rows) // move rows 2-3 to index 1 .whenComplete { _ in print ("yay moved") }
- Deleting rows/columns in a sheet:
spreadsheet.delete(sheetId: sheetId, range: 2..<3, to: 2, dimension: .rows) // deletes rows at indexes 2-3 .whenComplete { _ in print ("yay deleted") }
- Adding rows/columns in a sheet:
spreadsheet.create(title: "Name of the sheet", dimensions: .init(rowCount: 10, columnCount: 5)) .whenComplete { print ("yay created with ID: \($0.replies.first!.addSheet!.properties!.sheetId)") }
- Deleting a sheet from a spreadsheet:
spreadsheet.delete(sheetId: sheetId) .whenComplete { _ in print ("yay deleted") }
- Clearing a sheet:
spreadsheet.clear(sheetId: sheetId) // will delete all data in the sheet .whenComplete { _ in print ("yay cleared") }
Haven't documented IndexedSheet & AtomicSheet yet :/