feat: Added "Import" option in the account menu that allows you to import from plaintext/markdown files, Evernote exports, Simplenote exports, Google Keep exports and also convert Aegis exports to TokenVault notes

This commit is contained in:
Aman Harwara
2022-12-30 01:22:59 +05:30
committed by GitHub
parent f1ae7a53ef
commit 6e4bf2417a
25 changed files with 755 additions and 207 deletions

View File

@@ -3,7 +3,6 @@ import { ContentType } from '@standardnotes/common'
import { readFileAsText } from '../Utils'
import { FeatureIdentifier, NoteType } from '@standardnotes/features'
import { WebApplicationInterface } from '@standardnotes/services'
import { Importer } from '../Importer'
type AegisData = {
db: {
@@ -18,6 +17,8 @@ type AegisData = {
}
}
const AegisEntryTypes = ['hotp', 'totp', 'steam', 'yandex'] as const
type AuthenticatorEntry = {
service: string
account: string
@@ -25,9 +26,26 @@ type AuthenticatorEntry = {
notes: string
}
export class AegisToAuthenticatorConverter extends Importer {
constructor(protected override application: WebApplicationInterface) {
super(application)
export class AegisToAuthenticatorConverter {
constructor(protected application: WebApplicationInterface) {}
static isValidAegisJson(json: any): boolean {
return json.db && json.db.entries && json.db.entries.every((entry: any) => AegisEntryTypes.includes(entry.type))
}
async convertAegisBackupFileToNote(
file: File,
addEditorInfo: boolean,
): Promise<DecryptedTransferPayload<NoteContent>> {
const content = await readFileAsText(file)
const entries = this.parseEntries(content)
if (!entries) {
throw new Error('Could not parse entries')
}
return this.createNoteFromEntries(entries, file, addEditorInfo)
}
createNoteFromEntries(
@@ -57,21 +75,6 @@ export class AegisToAuthenticatorConverter extends Importer {
}
}
async convertAegisBackupFileToNote(
file: File,
addEditorInfo: boolean,
): Promise<DecryptedTransferPayload<NoteContent>> {
const content = await readFileAsText(file)
const entries = this.parseEntries(content)
if (!entries) {
throw new Error('Could not parse entries')
}
return this.createNoteFromEntries(entries, file, addEditorInfo)
}
parseEntries(data: string): AuthenticatorEntry[] | null {
try {
const json = JSON.parse(data) as AegisData

View File

@@ -2,7 +2,6 @@ import { ContentType } from '@standardnotes/common'
import { WebApplicationInterface } from '@standardnotes/services'
import { DecryptedTransferPayload, NoteContent, TagContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils'
import { Importer } from '../Importer'
import dayjs from 'dayjs'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import utc from 'dayjs/plugin/utc'
@@ -11,10 +10,8 @@ dayjs.extend(utc)
const dateFormat = 'YYYYMMDDTHHmmss'
export class EvernoteConverter extends Importer {
constructor(protected override application: WebApplicationInterface) {
super(application)
}
export class EvernoteConverter {
constructor(protected application: WebApplicationInterface) {}
async convertENEXFileToNotesAndTags(file: File, stripHTML: boolean): Promise<DecryptedTransferPayload[]> {
const content = await readFileAsText(file)
@@ -138,6 +135,9 @@ export class EvernoteConverter extends Importer {
}
const allItems: DecryptedTransferPayload[] = [...notes, ...tags]
if (allItems.length === 0) {
throw new Error('Could not parse any notes or tags from Evernote file.')
}
if (defaultTag) {
allItems.push(defaultTag)
}

View File

@@ -2,7 +2,6 @@ import { WebApplicationInterface } from '@standardnotes/services'
import { ContentType } from '@standardnotes/common'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { readFileAsText } from '../Utils'
import { Importer } from '../Importer'
type GoogleKeepJsonNote = {
color: string
@@ -14,10 +13,8 @@ type GoogleKeepJsonNote = {
userEditedTimestampUsec: number
}
export class GoogleKeepConverter extends Importer {
constructor(protected override application: WebApplicationInterface) {
super(application)
}
export class GoogleKeepConverter {
constructor(protected application: WebApplicationInterface) {}
async convertGoogleKeepBackupFileToNote(
file: File,
@@ -99,9 +96,24 @@ export class GoogleKeepConverter extends Importer {
return
}
static isValidGoogleKeepJson(json: any): boolean {
return (
json.title &&
json.textContent &&
json.userEditedTimestampUsec &&
typeof json.isArchived === 'boolean' &&
typeof json.isTrashed === 'boolean' &&
typeof json.isPinned === 'boolean' &&
json.color
)
}
tryParseAsJson(data: string): DecryptedTransferPayload<NoteContent> | null {
try {
const parsed = JSON.parse(data) as GoogleKeepJsonNote
if (!GoogleKeepConverter.isValidGoogleKeepJson(parsed)) {
return null
}
const date = new Date(parsed.userEditedTimestampUsec / 1000)
return {
created_at: date,

View File

@@ -1,14 +1,91 @@
import { WebApplicationInterface } from '@standardnotes/services'
import { DecryptedTransferPayload } from '@standardnotes/snjs'
import { parseFileName } from '@standardnotes/filepicker'
import { FeatureStatus, WebApplicationInterface } from '@standardnotes/services'
import { FeatureIdentifier } from '@standardnotes/features'
import { AegisToAuthenticatorConverter } from './AegisConverter/AegisToAuthenticatorConverter'
import { EvernoteConverter } from './EvernoteConverter/EvernoteConverter'
import { GoogleKeepConverter } from './GoogleKeepConverter/GoogleKeepConverter'
import { PlaintextConverter } from './PlaintextConverter/PlaintextConverter'
import { SimplenoteConverter } from './SimplenoteConverter/SimplenoteConverter'
import { readFileAsText } from './Utils'
import { DecryptedTransferPayload } from '@standardnotes/models'
export type NoteImportType = 'plaintext' | 'evernote' | 'google-keep' | 'simplenote' | 'aegis'
export class Importer {
constructor(protected application: WebApplicationInterface) {}
aegisConverter: AegisToAuthenticatorConverter
googleKeepConverter: GoogleKeepConverter
simplenoteConverter: SimplenoteConverter
plaintextConverter: PlaintextConverter
evernoteConverter: EvernoteConverter
async importFromTransferPayloads(payloads: DecryptedTransferPayload[]): Promise<void> {
for (const payload of payloads) {
const itemPayload = this.application.items.createPayloadFromObject(payload)
const item = this.application.items.createItemFromPayload(itemPayload)
await this.application.mutator.insertItem(item)
constructor(protected application: WebApplicationInterface) {
this.aegisConverter = new AegisToAuthenticatorConverter(application)
this.googleKeepConverter = new GoogleKeepConverter(application)
this.simplenoteConverter = new SimplenoteConverter(application)
this.plaintextConverter = new PlaintextConverter(application)
this.evernoteConverter = new EvernoteConverter(application)
}
static detectService = async (file: File): Promise<NoteImportType | null> => {
const content = await readFileAsText(file)
const { ext } = parseFileName(file.name)
if (ext === 'enex') {
return 'evernote'
}
try {
const json = JSON.parse(content)
if (AegisToAuthenticatorConverter.isValidAegisJson(json)) {
return 'aegis'
}
if (GoogleKeepConverter.isValidGoogleKeepJson(json)) {
return 'google-keep'
}
if (SimplenoteConverter.isValidSimplenoteJson(json)) {
return 'simplenote'
}
} catch {
/* empty */
}
if (PlaintextConverter.isValidPlaintextFile(file)) {
return 'plaintext'
}
return null
}
async getPayloadsFromFile(file: File, type: NoteImportType): Promise<DecryptedTransferPayload[]> {
if (type === 'aegis') {
const isEntitledToAuthenticator =
this.application.features.getFeatureStatus(FeatureIdentifier.TokenVaultEditor) === FeatureStatus.Entitled
return [await this.aegisConverter.convertAegisBackupFileToNote(file, isEntitledToAuthenticator)]
} else if (type === 'google-keep') {
return [await this.googleKeepConverter.convertGoogleKeepBackupFileToNote(file, true)]
} else if (type === 'simplenote') {
return await this.simplenoteConverter.convertSimplenoteBackupFileToNotes(file)
} else if (type === 'evernote') {
return await this.evernoteConverter.convertENEXFileToNotesAndTags(file, false)
} else if (type === 'plaintext') {
return [await this.plaintextConverter.convertPlaintextFileToNote(file)]
}
return []
}
async importFromTransferPayloads(payloads: DecryptedTransferPayload[]) {
const insertedItems = await Promise.all(
payloads.map(async (payload) => {
const itemPayload = this.application.items.createPayloadFromObject(payload)
const item = this.application.items.createItemFromPayload(itemPayload)
return this.application.mutator.insertItem(item)
}),
)
return insertedItems
}
}

View File

@@ -0,0 +1,36 @@
import { ContentType } from '@standardnotes/common'
import { parseFileName } from '@standardnotes/filepicker'
import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { WebApplicationInterface } from '@standardnotes/services'
import { readFileAsText } from '../Utils'
export class PlaintextConverter {
constructor(protected application: WebApplicationInterface) {}
static isValidPlaintextFile(file: File): boolean {
return file.type === 'text/plain' || file.type === 'text/markdown'
}
async convertPlaintextFileToNote(file: File): Promise<DecryptedTransferPayload<NoteContent>> {
const content = await readFileAsText(file)
const { name } = parseFileName(file.name)
const createdAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
const updatedAtDate = file.lastModified ? new Date(file.lastModified) : new Date()
return {
created_at: createdAtDate,
created_at_timestamp: createdAtDate.getTime(),
updated_at: updatedAtDate,
updated_at_timestamp: updatedAtDate.getTime(),
uuid: this.application.generateUUID(),
content_type: ContentType.Note,
content: {
title: name,
text: content,
references: [],
},
}
}
}

View File

@@ -2,7 +2,6 @@ import { DecryptedTransferPayload, NoteContent } from '@standardnotes/models'
import { ContentType } from '@standardnotes/common'
import { readFileAsText } from '../Utils'
import { WebApplicationInterface } from '@standardnotes/services'
import { Importer } from '../Importer'
type SimplenoteItem = {
creationDate: string
@@ -15,9 +14,28 @@ type SimplenoteData = {
trashedNotes: SimplenoteItem[]
}
export class SimplenoteConverter extends Importer {
constructor(protected override application: WebApplicationInterface) {
super(application)
const isSimplenoteEntry = (entry: any): boolean => entry.id && entry.content && entry.creationDate && entry.lastModified
export class SimplenoteConverter {
constructor(protected application: WebApplicationInterface) {}
static isValidSimplenoteJson(json: any): boolean {
return (
(json.activeNotes && json.activeNotes.every(isSimplenoteEntry)) ||
(json.trashedNotes && json.trashedNotes.every(isSimplenoteEntry))
)
}
async convertSimplenoteBackupFileToNotes(file: File): Promise<DecryptedTransferPayload<NoteContent>[]> {
const content = await readFileAsText(file)
const notes = this.parse(content)
if (!notes) {
throw new Error('Could not parse notes')
}
return notes
}
createNoteFromItem(item: SimplenoteItem, trashed: boolean): DecryptedTransferPayload<NoteContent> {
@@ -51,18 +69,6 @@ export class SimplenoteConverter extends Importer {
}
}
async convertSimplenoteBackupFileToNotes(file: File): Promise<DecryptedTransferPayload<NoteContent>[]> {
const content = await readFileAsText(file)
const notes = this.parse(content)
if (!notes) {
throw new Error('Could not parse notes')
}
return notes
}
parse(data: string) {
try {
const parsed = JSON.parse(data) as SimplenoteData

View File

@@ -0,0 +1,7 @@
export * from './AegisConverter/AegisToAuthenticatorConverter'
export * from './SimplenoteConverter/SimplenoteConverter'
export * from './GoogleKeepConverter/GoogleKeepConverter'
export * from './EvernoteConverter/EvernoteConverter'
export * from './PlaintextConverter/PlaintextConverter'
export * from './Utils'
export * from './Importer'

View File

@@ -32,7 +32,4 @@ export * from './Theme/ThemeManager'
export * from './Toast/ToastService'
export * from './Toast/ToastServiceInterface'
export * from './StatePersistence/StatePersistence'
export * from './Import/AegisConverter/AegisToAuthenticatorConverter'
export * from './Import/SimplenoteConverter/SimplenoteConverter'
export * from './Import/GoogleKeepConverter/GoogleKeepConverter'
export * from './Import/EvernoteConverter/EvernoteConverter'
export * from './Import'