Skip to content

Commit

Permalink
feat: integrate onenote
Browse files Browse the repository at this point in the history
  • Loading branch information
shuowu committed May 12, 2020
1 parent 8410922 commit 6bba861
Show file tree
Hide file tree
Showing 7 changed files with 239 additions and 22 deletions.
19 changes: 18 additions & 1 deletion extension/src/background/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import Logger from 'js-logger';
import migration_v_0_6_4 from './migrations/0.6.4';
import Evernote from './integrations/evernote';
import { Evernote, OneNote } from './integrations';

Logger.useDefaults();

Expand Down Expand Up @@ -38,6 +38,20 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
});
};

const saveToOneNote = () => {
const { data } = message;
const oneNote = new OneNote();
oneNote
.saveNotes(data)
.then(data => {
sendResponse({ onenoteId: data.guid });
})
.catch(e => {
logger.error(e);
sendResponse(e);
});
};

const { action } = message;
switch (action) {
case 'open-options':
Expand All @@ -49,6 +63,9 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
case 'save-to-evernote':
saveToEvernote();
return true;
case 'save-to-onenote':
saveToOneNote();
return true;
}
});

Expand Down
38 changes: 17 additions & 21 deletions extension/src/background/integrations/evernote.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import EvernoteSDK from 'evernote';
import md5 from 'md5';
import bufferFrom from 'buffer-from';
import { getRedirectUrl, enhancedFetch } from './utils';
import { secondsToTime, addQueryToUrl } from '../../common/utils';
import { QUERY_AUTO_JUMP } from '../../constants';
import bufferFrom from 'buffer-from';

const enhancedFetch = (path, request) =>
fetch(`${process.env.REST_BASE_URL}${path}`, request).then(res => {
if (res.ok) {
return res.json();
}
throw res.text();
});
const PROVIDER = 'evernote';

const escape = url => {
var tagsToReplace = {
Expand All @@ -30,31 +25,27 @@ class Evernote {
this.storage = browser.storage.local;
}

getRedirectURL() {
return `${browser.identity.getRedirectURL()}provider_cb`;
}

cacheAccessToken(accessToken) {
return this.storage.set({ evernote: { accessToken } });
}

clearAccessToken() {
return this.storage.remove('evernote');
return this.storage.remove(PROVIDER);
}

getAccessToken() {
return new Promise((resolve, reject) => {
this.storage.get('evernote').then(data => {
this.storage.get(PROVIDER).then(data => {
const { accessToken } = data.evernote || {};
if (accessToken) {
resolve(accessToken);
return;
}

// OAuth flow to get accessToken
const redirectUrl = this.getRedirectURL();
const redirectUrl = getRedirectUrl(PROVIDER);
enhancedFetch(
`/evernote/authorize-url?redirectUrl=${redirectUrl}`
`${process.env.REST_BASE_URL}/evernote/authorize-url?redirectUrl=${redirectUrl}`
).then(data => {
const { oauthUrl, oauthToken, oauthSecret } = data;
browser.identity
Expand All @@ -64,10 +55,13 @@ class Evernote {
const params = new URLSearchParams(parsedUrl.search);
const verifier = params.get('oauth_verifier');

enhancedFetch('/evernote/access-token', {
method: 'POST',
body: JSON.stringify({ oauthToken, oauthSecret, verifier })
}).then(({ accessToken }) => {
enhancedFetch(
`${process.env.REST_BASE_URL}/evernote/access-token`,
{
method: 'POST',
body: JSON.stringify({ oauthToken, oauthSecret, verifier })
}
).then(({ accessToken }) => {
this.cacheAccessToken(accessToken).then(() => {
resolve(accessToken);
});
Expand All @@ -89,7 +83,9 @@ class Evernote {
nBody += '<en-note>';
nBody += `<span>Notes are auto-generated from <a style="padding-bottom: 20px;" href="https://github.com/shuowu/yi-note#installation">YiNote browser extension</a></span>`;
nBody += '<br/><br/>';
nBody += `<a style="padding-bottom: 20px;" href="${escape(url)}">${browser.i18n.getMessage('evernote_note_origin')}</a>`;
nBody += `<a style="padding-bottom: 20px;" href="${escape(
url
)}">${browser.i18n.getMessage('evernote_note_origin')}</a>`;
nBody += '<br/><br/>';
nBody += `<div>${description}</div>`;
nBody += '<br/>';
Expand Down
2 changes: 2 additions & 0 deletions extension/src/background/integrations/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { default as Evernote } from './evernote';
export { default as OneNote } from './onenote';
173 changes: 173 additions & 0 deletions extension/src/background/integrations/onenote.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
import { uuid } from 'uuidv4';
import merge from 'deepmerge';
import { getRedirectUrl, enhancedFetch } from './utils';
import { secondsToTime, getUrlWithTimestamp } from '../../common/utils';

const PROVIDER = 'onenote';
const CLIENT_ID = 'ab2e71d8-340a-4889-8039-26b70504871c';
const YiNotebookName = 'YiNotebook';

const getRequestUrl = () => {
let url = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize?';
url += '&response_type=token';
url += '&scope=notes.create';
url += `&client_id=${CLIENT_ID}`;
url += `&redirect_uri=${encodeURIComponent(getRedirectUrl(PROVIDER))}`;
url += `&state=${uuid()}`;
return url;
};

const getParamsFromCallbackUrl = url => {
const parsedUrl = new URL(url);
return parsedUrl.hash
.substring(1)
.split('&')
.reduce((params, part) => {
const parts = part.split('=');
params[parts[0]] = parts[1];
return params;
}, {});
};

const generatePage = (meta, notes) => `
<!DOCTYPE html>
<html>
<head>
<title>${meta.title}</title>
</head>
<body>
<span>
Generated from
<a href="https://github.com/shuowu/yi-note#installation">YiNote browser extension</a>
</span>
<p>${meta.description}</p>
<div>
${notes.map(note => {
return `
<div>
<img src=${note.image} />
<span>
<a href="${getUrlWithTimestamp(meta.url, note.timestamp)}">
${secondsToTime(note.timestamp)}
</a>
</span>
<p>${note.content}</p>
</div>
`;
})}
</div>
</body>
</html>
`;

class OneNote {
client;
storage;

constructor() {
this.storage = browser.storage.local;
}

cacheAccessToken(accessToken) {
return this.storage.set({ [PROVIDER]: { accessToken } });
}

clearAccessToken() {
return this.storage.remove(PROVIDER);
}

getAccessTokenFromCache() {
return this.storage.get(PROVIDER).then(data => {
const providerData = data[PROVIDER] || {};
return providerData.accessToken;
});
}

getAccessToken() {
return new Promise((resolve, reject) => {
this.getAccessTokenFromCache().then(accessToken => {
if (accessToken) {
resolve(accessToken);
return;
}

// OAuth2 implicit flow to get accessToken
const requestUrl = getRequestUrl();
browser.identity
.launchWebAuthFlow({ interactive: true, url: requestUrl })
.then(url => {
const params = getParamsFromCallbackUrl(url);
const accessToken = params.access_token;
this.cacheAccessToken(accessToken).then(() => {
resolve(accessToken);
});
})
.catch(url => {
const params = getParamsFromCallbackUrl(url);
reject(params.error);
});
});
});
}

api(path, request = {}) {
const baseUrl = 'https://graph.microsoft.com/v1.0/me/onenote';
return this.getAccessToken().then(token => {
const defaultRequest = {
withCredentials: true,
credentials: 'include',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json'
}
};
return enhancedFetch(
`${baseUrl}${path}`,
merge(defaultRequest, request)
).catch(({ error }) => {
return this.getAccessTokenFromCache().then(token => {
if (token && error.code === 'InvalidAuthenticationToken') {
return this.clearAccessToken().then(() => this.api(path, request));
}
throw error;
});
});
});
}

async saveNotes({ meta, notes }) {
try {
const { value: notebooks } = await this.api('/notebooks');
let notebook = notebooks.find(
({ displayName }) => displayName === YiNotebookName
);
if (!notebook) {
notebook = await this.api('/notebooks', {
method: 'POST',
body: JSON.stringify({ displayName: YiNotebookName })
});
}
const { value: sections } = await this.api('/sections');
let section = sections.find(
({ displayName }) => displayName === 'YiNote'
);
if (!section) {
section = await this.api(`/notebooks/${notebook.id}/sections`, {
method: 'POST',
body: JSON.stringify({ displayName: 'YiNote' })
});
}
const page = await this.api(`/sections/${section.id}/pages`, {
method: 'POST',
headers: {
'Content-Type': 'text/html'
},
body: generatePage(meta, notes)
});
} catch (e) {
console.log(e);
}
}
}

export default OneNote;
20 changes: 20 additions & 0 deletions extension/src/background/integrations/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export const getRedirectUrl = provider => {
return `${browser.identity.getRedirectURL()}${provider}`;
};

export const enhancedFetch = (url, request) =>
fetch(url, request).then(res => {
if (res.ok) {
return res.json();
}
return res.text().then(body => {
let err;
try {
err = JSON.parse(body);
} catch (e) {
err = body;
}

throw err;
});
});
8 changes: 8 additions & 0 deletions extension/src/common/utils/getUrlWithTimestamp.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { QUERY_AUTO_JUMP } from '../../constants';
import addQueryToUrl from './addQueryToUrl';

export default (url, timestamp) => {
let res = addQueryToUrl(url, QUERY_AUTO_JUMP, timestamp);
res = addQueryToUrl(res, 't', `${timestamp}s`);
return res;
};
1 change: 1 addition & 0 deletions extension/src/common/utils/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export { default as secondsToTime } from './secondsToTime';
export { default as getVersion } from './getVersion';
export { default as generatePageId } from './generatePageId';
export { default as addQueryToUrl } from './addQueryToUrl';
export { default as getUrlWithTimestamp } from './getUrlWithTimestamp';

0 comments on commit 6bba861

Please sign in to comment.