-
Notifications
You must be signed in to change notification settings - Fork 2.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added Outlook integration toolkit #3465
base: main
Are you sure you want to change the base?
Changes from 28 commits
12dd2a9
3388d62
2694daf
884bab7
929f9f8
44928c1
ba52666
106868d
3b54b6f
5c259c7
3d123c5
df52718
5fc9972
43388c7
497e486
e29c32d
11d91fc
d19b446
7091118
d48ab61
6234417
d38f9f6
1bc7761
0ff7f6d
12d160f
cf52d46
7c03901
083a8ae
d70da41
918ad47
9a6ef37
43ee152
d4bf536
d21fc40
960d082
f4861fe
bdb51ab
c518e9f
3212a3c
767acbc
216c3cc
c94886e
c3f6c74
3cec11e
9b1d3f4
d92e771
6ebb241
da61a07
ed41abe
ba2ec31
d9ca68d
04a7221
23271fd
562df12
a9ba549
7fa8d85
c2a9eba
c7eb5c6
6718669
4cbdeb9
b300654
1fb53e0
4719a52
cdde1d4
ab49c28
175324c
8751341
42189f8
f94225a
d1e961e
569b34e
981242e
1aa5ee3
d03bd77
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -82,6 +82,9 @@ | |
"tools/google_calendar.cjs", | ||
"tools/google_calendar.js", | ||
"tools/google_calendar.d.ts", | ||
"tools/outlook.cjs", | ||
"tools/outlook.js", | ||
"tools/outlook.d.ts", | ||
"tools/google_places.cjs", | ||
"tools/google_places.js", | ||
"tools/google_places.d.ts", | ||
|
@@ -921,6 +924,7 @@ | |
"@types/jsdom": "^21.1.1", | ||
"@types/lodash": "^4", | ||
"@types/mozilla-readability": "^0.2.1", | ||
"@types/openurl": "^1", | ||
"@types/pdf-parse": "^1.1.1", | ||
"@types/pg": "^8", | ||
"@types/pg-copy-streams": "^1.2.2", | ||
|
@@ -1424,6 +1428,7 @@ | |
"binary-extensions": "^2.2.0", | ||
"expr-eval": "^2.0.2", | ||
"flat": "^5.0.2", | ||
"http": "^0.0.1-security", | ||
"js-tiktoken": "^1.0.7", | ||
"js-yaml": "^4.1.0", | ||
"jsonpointer": "^5.0.1", | ||
|
@@ -1432,7 +1437,10 @@ | |
"ml-distance": "^4.0.0", | ||
"openai": "^4.19.0", | ||
"openapi-types": "^12.1.3", | ||
"openurl": "^1.1.1", | ||
"p-queue": "^6.6.2", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove extra deps |
||
"p-retry": "4", | ||
"url": "^0.11.3", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is necessary? There is a web built-in There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yep, no need, I will remove this |
||
"uuid": "^9.0.0", | ||
"yaml": "^2.2.1", | ||
"zod": "^3.22.3", | ||
|
@@ -1576,6 +1584,11 @@ | |
"import": "./tools/google_calendar.js", | ||
"require": "./tools/google_calendar.cjs" | ||
}, | ||
"./tools/outlook": { | ||
"types": "./tools/outlook.d.ts", | ||
"import": "./tools/outlook.js", | ||
"require": "./tools/outlook.cjs" | ||
}, | ||
"./tools/google_places": { | ||
"types": "./tools/google_places.d.ts", | ||
"import": "./tools/google_places.js", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,3 +47,10 @@ export { | |
formatToOpenAIFunction, | ||
formatToOpenAITool, | ||
} from "./convert_to_openai.js"; | ||
|
||
export { OutlookSendMailTool, OutlookReadMailTool } from "./outlook/index.js"; | ||
export { | ||
GooglePlacesAPI, | ||
type GooglePlacesAPIParams, | ||
} from "./google_places.js"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove from |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
export abstract class AuthFlowBase { | ||
protected clientId: string; | ||
|
||
protected accessToken = ""; | ||
|
||
constructor(clientId: string) { | ||
this.clientId = clientId; | ||
} | ||
|
||
public abstract getAccessToken(): Promise<string>; | ||
|
||
public abstract refreshAccessToken(): Promise<string>; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,189 @@ | ||
import * as http from "http"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR adds a new HTTP request using the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This PR adds code that explicitly accesses and reads environment variables via the |
||
import * as url from "url"; | ||
import * as openurl from "openurl"; | ||
import { AuthFlowBase } from "./authFlowBase.js"; | ||
import { getEnvironmentVariable } from "../../util/env.js"; | ||
|
||
interface AccessTokenResponse { | ||
access_token: string; | ||
refresh_token: string; | ||
} | ||
|
||
export class AuthFlowREST extends AuthFlowBase { | ||
private clientSecret: string; | ||
|
||
private redirectUri: string; | ||
|
||
private port: number; | ||
|
||
private pathname: string; | ||
|
||
private refreshToken = ""; | ||
|
||
constructor({ clientId, clientSecret, redirectUri }: { clientId?: string, clientSecret?: string, redirectUri?: string } = {}) { | ||
let id = clientId; | ||
let secret = clientSecret; | ||
let uri = redirectUri; | ||
if (!id || !secret || !uri) { | ||
id = getEnvironmentVariable("OUTLOOK_CLIENT_ID"); | ||
secret = getEnvironmentVariable("OUTLOOK_CLIENT_SECRET"); | ||
uri = getEnvironmentVariable("OUTLOOK_REDIRECT_URI"); | ||
} | ||
Qi123123Li marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if (!id || !secret || !uri) { | ||
throw new Error("Missing clientId, clientSecret or redirectUri."); | ||
} | ||
super(id); | ||
this.clientSecret = secret; | ||
this.redirectUri = uri; | ||
const parsedUrl = new URL(this.redirectUri); | ||
this.port = parsedUrl.port ? parseInt(parsedUrl.port, 10) : 3000; | ||
this.pathname = parsedUrl.pathname || ""; | ||
} | ||
|
||
// Function to construct the OAuth URL | ||
private openAuthUrl(): string { | ||
const loginEndpoint = | ||
"https://login.microsoftonline.com/common/oauth2/v2.0/authorize"; | ||
const { clientId } = this; // client ID regestered in Azure | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No need to do object destructuring here, since it's only used in one place it's clearer to just call |
||
const response_type = "code"; | ||
const response_mode = "query"; | ||
const redirectUri = encodeURIComponent(this.redirectUri); // redirect URI regestered in Azure | ||
const scope = encodeURIComponent( | ||
"openid offline_access https://graph.microsoft.com/.default" | ||
); | ||
const state = "12345"; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Double checking this is the correct value? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just found that this is optional so I will remove this |
||
|
||
const url = [ | ||
`${loginEndpoint}?client_id=${clientId}`, | ||
`&response_type=${response_type}`, | ||
`&response_mode=${response_mode}`, | ||
`&redirect_uri=${redirectUri}`, | ||
`&scope=${scope}`, | ||
`&state=${state}`, | ||
].join(""); | ||
openurl.open(url); | ||
return url; | ||
} | ||
|
||
// Function to start the server | ||
private startServer(): Promise<http.Server> { | ||
return new Promise((resolve, reject) => { | ||
const server = http.createServer(); | ||
|
||
server.listen(this.port, () => { | ||
console.log(`Server listening at http://localhost:${this.port}`); | ||
resolve(server); | ||
}); | ||
|
||
server.on("error", (err) => { | ||
reject(err); | ||
}); | ||
}); | ||
} | ||
|
||
// Function to listen for the authorization code | ||
private async listenForCode(server: http.Server): Promise<string> { | ||
return new Promise((resolve, reject) => { | ||
server.on("request", (req, res) => { | ||
try { | ||
const reqUrl = url.parse(req.url || "", true); | ||
|
||
if (reqUrl.pathname === this.pathname) { | ||
const authCode = reqUrl.query.code as string; | ||
|
||
res.writeHead(200, { "Content-Type": "text/html" }); | ||
res.end("Authorization code received. You can close this window."); | ||
|
||
server.close(); | ||
console.log("Server closed"); | ||
resolve(authCode); // Resolve the Promise with the authorization code | ||
} else { | ||
res.writeHead(404); | ||
res.end("404 Not Found"); | ||
} | ||
} catch (err) { | ||
res.writeHead(500); | ||
res.end("Server error"); | ||
reject(err); | ||
} | ||
}); | ||
}); | ||
} | ||
|
||
// Main function to run the auth flow | ||
private async getCode(): Promise<string> { | ||
// check credentials | ||
if (!this.clientId || !this.redirectUri) { | ||
throw new Error("Missing clientId or redirectUri."); | ||
} | ||
|
||
const server = await this.startServer(); | ||
this.openAuthUrl(); | ||
const code = await this.listenForCode(server); | ||
return code; | ||
} | ||
|
||
// Function to get the token using the code and client credentials | ||
public async getAccessToken(): Promise<string> { | ||
// fetch auth code from user login | ||
const code = await this.getCode(); | ||
// fetch access token using auth code | ||
const req_body = | ||
`client_id=${encodeURIComponent(this.clientId)}&` + | ||
`client_secret=${encodeURIComponent(this.clientSecret)}&` + | ||
`scope=${encodeURIComponent("https://graph.microsoft.com/.default")}&` + | ||
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` + | ||
`grant_type=authorization_code&` + | ||
`code=${encodeURIComponent(code)}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could be cleaner to use something like |
||
|
||
const response = await fetch( | ||
"https://login.microsoftonline.com/common/oauth2/v2.0/token", | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: req_body, | ||
} | ||
); | ||
|
||
if (!response.ok) { | ||
throw new Error(`fetch token error! response: ${response.status}`); | ||
} | ||
// save access token and refresh token | ||
const json = (await response.json()) as AccessTokenResponse; | ||
this.accessToken = json.access_token; | ||
this.refreshToken = json.refresh_token; | ||
return this.accessToken; | ||
} | ||
|
||
public async refreshAccessToken(): Promise<string> { | ||
// fetch new access token using refresh token | ||
const req_body = | ||
`client_id=${encodeURIComponent(this.clientId)}&` + | ||
`client_secret=${encodeURIComponent(this.clientSecret)}&` + | ||
`scope=${encodeURIComponent("https://graph.microsoft.com/.default")}&` + | ||
`redirect_uri=${encodeURIComponent(this.redirectUri)}&` + | ||
`grant_type=refresh_token&` + | ||
`refresh_token=${encodeURIComponent(this.refreshToken)}`; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same search params comment here |
||
|
||
const response = await fetch( | ||
"https://login.microsoftonline.com/common/oauth2/v2.0/token", | ||
{ | ||
method: "POST", | ||
headers: { | ||
"Content-Type": "application/x-www-form-urlencoded", | ||
}, | ||
body: req_body, | ||
} | ||
); | ||
|
||
if (!response.ok) { | ||
throw new Error(`fetch token error! response: ${response.status}`); | ||
} | ||
// save new access token | ||
const json = (await response.json()) as AccessTokenResponse; | ||
this.accessToken = json.access_token; | ||
return this.accessToken; | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks like a mistake?