mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge branch 'main' into discover-no-ts-changes
This commit is contained in:
commit
e47e169402
70 changed files with 2687 additions and 532 deletions
|
|
@ -4,7 +4,6 @@
|
|||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
|
||||
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<application
|
||||
|
|
|
|||
|
|
@ -239,8 +239,8 @@
|
|||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/nathantannar4/Transmission",
|
||||
"state" : {
|
||||
"revision" : "3dac53ae4bddc7ab99e6374622a9c5eefbe50eed",
|
||||
"version" : "1.1.4"
|
||||
"revision" : "9517912f8f528c777f86f7896b5c35d7e43fa916",
|
||||
"version" : "1.0.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -71,9 +71,9 @@ var dependencies: [Package.Dependency] {
|
|||
.package(url: "https://github.com/google/GoogleSignIn-iOS", from: "6.2.2"),
|
||||
.package(url: "https://github.com/gonzalezreal/swift-markdown-ui", from: "2.0.0"),
|
||||
.package(url: "https://github.com/PostHog/posthog-ios.git", from: "2.0.0"),
|
||||
.package(url: "https://github.com/nathantannar4/Engine", exact: "1.5.1"),
|
||||
.package(url: "https://github.com/nathantannar4/Turbocharger", exact: "1.1.4"),
|
||||
.package(url: "https://github.com/nathantannar4/Transmission", from: "1.1.4")
|
||||
// .package(url: "https://github.com/nathantannar4/Engine", exact: "1.0.1"),
|
||||
// .package(url: "https://github.com/nathantannar4/Turbocharger", exact: "1.1.4"),
|
||||
.package(url: "https://github.com/nathantannar4/Transmission", exact: "1.0.1")
|
||||
]
|
||||
// Comment out following line for macOS build
|
||||
deps.append(.package(url: "https://github.com/PSPDFKit/PSPDFKit-SP", from: "13.1.0"))
|
||||
|
|
|
|||
|
|
@ -155,12 +155,21 @@ struct ProfileView: View {
|
|||
label: { Text(LocalText.documentationGeneric) }
|
||||
)
|
||||
|
||||
#if os(iOS)
|
||||
Button(
|
||||
action: { DataService.showIntercomMessenger?() },
|
||||
label: { Text(LocalText.feedbackGeneric) }
|
||||
)
|
||||
#endif
|
||||
#if os(iOS)
|
||||
Button(
|
||||
action: { DataService.showIntercomMessenger?() },
|
||||
label: { Text(LocalText.feedbackGeneric) }
|
||||
)
|
||||
#endif
|
||||
|
||||
Button(
|
||||
action: {
|
||||
if let url = URL(string: "https://apps.apple.com/app/id1564031042?action=write-review") {
|
||||
openURL(url)
|
||||
}
|
||||
},
|
||||
label: { Text("Review Omnivore") }
|
||||
)
|
||||
|
||||
Button(
|
||||
action: {
|
||||
|
|
@ -170,7 +179,9 @@ struct ProfileView: View {
|
|||
},
|
||||
label: { Text("Join community on Discord") }
|
||||
)
|
||||
}
|
||||
|
||||
Section {
|
||||
Button(
|
||||
action: {
|
||||
if let url = URL(string: "https://omnivore.app/privacy") {
|
||||
|
|
|
|||
|
|
@ -98,11 +98,11 @@ typealias OperationStatusHandler = (_: OperationStatus) -> Void
|
|||
}
|
||||
}
|
||||
|
||||
func updateSubscription(dataService: DataService, subscription: Subscription, folder: String? = nil, fetchContent: Bool? = nil) async {
|
||||
func updateSubscription(dataService: DataService, subscription: Subscription, folder: String? = nil, fetchContentType: FetchContentType? = nil) async {
|
||||
operationMessage = "Updating subscription..."
|
||||
operationStatus = .isPerforming
|
||||
do {
|
||||
try await dataService.updateSubscription(subscription.subscriptionID, folder: folder, fetchContent: fetchContent)
|
||||
try await dataService.updateSubscription(subscription.subscriptionID, folder: folder, fetchContentType: fetchContentType)
|
||||
operationMessage = "Subscription updated"
|
||||
operationStatus = .success
|
||||
} catch {
|
||||
|
|
@ -240,23 +240,27 @@ struct SubscriptionsView: View {
|
|||
#endif
|
||||
}
|
||||
|
||||
private var emptyView: some View {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("You don't have any Feed items.")
|
||||
.font(Font.system(size: 18, weight: .bold))
|
||||
|
||||
Text("Add an RSS/Atom feed")
|
||||
.foregroundColor(Color.blue)
|
||||
.onTapGesture {
|
||||
showAddFeedView = true
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 80)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
}
|
||||
|
||||
private var innerBody: some View {
|
||||
Group {
|
||||
List {
|
||||
Section("Feeds") {
|
||||
if viewModel.feeds.count <= 0, !viewModel.isLoading {
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("You don't have any Feed items.")
|
||||
.font(Font.system(size: 18, weight: .bold))
|
||||
|
||||
Text("Add an RSS/Atom feed")
|
||||
.foregroundColor(Color.blue)
|
||||
.onTapGesture {
|
||||
showAddFeedView = true
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 80)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
emptyView
|
||||
} else {
|
||||
ForEach(viewModel.feeds, id: \.subscriptionID) { subscription in
|
||||
PresentationLink(transition: UIDevice.isIPad ? .popover : .sheet(detents: [.medium])) {
|
||||
|
|
@ -264,7 +268,7 @@ struct SubscriptionsView: View {
|
|||
subscription: subscription,
|
||||
viewModel: viewModel,
|
||||
dataService: dataService,
|
||||
prefetchContent: subscription.fetchContent,
|
||||
fetchContentType: subscription.fetchContentType,
|
||||
folderSelection: subscription.folder,
|
||||
unsubscribe: { _ in
|
||||
viewModel.operationStatus = .isPerforming
|
||||
|
|
@ -296,7 +300,7 @@ struct SubscriptionsView: View {
|
|||
subscription: subscription,
|
||||
viewModel: viewModel,
|
||||
dataService: dataService,
|
||||
prefetchContent: subscription.fetchContent,
|
||||
fetchContentType: subscription.fetchContentType,
|
||||
folderSelection: subscription.folder,
|
||||
unsubscribe: { _ in
|
||||
viewModel.operationStatus = .isPerforming
|
||||
|
|
@ -389,7 +393,7 @@ struct SubscriptionSettingsView: View {
|
|||
let viewModel: SubscriptionsViewModel
|
||||
let dataService: DataService
|
||||
|
||||
@State var prefetchContent = false
|
||||
@State var fetchContentType: FetchContentType
|
||||
@State var deleteConfirmationShown = false
|
||||
@State var showDeleteCompleted = false
|
||||
@State var folderSelection: String = ""
|
||||
|
|
@ -428,6 +432,28 @@ struct SubscriptionSettingsView: View {
|
|||
return nil
|
||||
}
|
||||
|
||||
var fetchContentRow: some View {
|
||||
Picker(selection: $fetchContentType, content: {
|
||||
Text("Always").tag(FetchContentType.always)
|
||||
Text("Never").tag(FetchContentType.never)
|
||||
Text("When empty").tag(FetchContentType.whenEmpty)
|
||||
}, label: { Text("Fetch link") })
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
.onChange(of: fetchContentType) { newValue in
|
||||
Task {
|
||||
viewModel.showOperationToast = true
|
||||
await viewModel.updateSubscription(
|
||||
dataService: dataService,
|
||||
subscription: subscription,
|
||||
fetchContentType: newValue
|
||||
)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
|
||||
viewModel.showOperationToast = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var folderRow: some View {
|
||||
HStack {
|
||||
Picker("Destination Folder", selection: $folderSelection) {
|
||||
|
|
@ -444,19 +470,6 @@ struct SubscriptionSettingsView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: prefetchContent) { newValue in
|
||||
Task {
|
||||
viewModel.showOperationToast = true
|
||||
await viewModel.updateSubscription(
|
||||
dataService: dataService,
|
||||
subscription: subscription,
|
||||
fetchContent: newValue
|
||||
)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(1500)) {
|
||||
viewModel.showOperationToast = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -570,12 +583,10 @@ struct SubscriptionSettingsView: View {
|
|||
.padding(.horizontal, 15)
|
||||
|
||||
List {
|
||||
// if subscription.type != .newsletter {
|
||||
// Toggle(isOn: $prefetchContent, label: { Text("Prefetch Content:") })
|
||||
// .onAppear {
|
||||
// prefetchContent = subscription.fetchContent
|
||||
// }
|
||||
// }
|
||||
if subscription.type != .newsletter {
|
||||
fetchContentRow
|
||||
}
|
||||
|
||||
folderRow
|
||||
labelRuleRow
|
||||
|
||||
|
|
|
|||
|
|
@ -66,6 +66,10 @@ struct WebReader: PlatformViewRepresentable {
|
|||
webView.scrollView.verticalScrollIndicatorInsets.top = readerViewNavBarHeight
|
||||
webView.configuration.userContentController.add(webView, name: "viewerAction")
|
||||
|
||||
if #available(iOS 15.4, *) {
|
||||
webView.configuration.preferences.isElementFullscreenEnabled = true
|
||||
}
|
||||
|
||||
webView.scrollView.indicatorStyle = ThemeManager.currentTheme.isDark ?
|
||||
UIScrollView.IndicatorStyle.white :
|
||||
UIScrollView.IndicatorStyle.black
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ public struct Subscription {
|
|||
public let name: String
|
||||
public let type: SubscriptionType
|
||||
public let folder: String
|
||||
public let fetchContent: Bool
|
||||
public let fetchContentType: FetchContentType
|
||||
public let newsletterEmailAddress: String?
|
||||
public let status: SubscriptionStatus
|
||||
public let unsubscribeHttpUrl: String?
|
||||
|
|
@ -24,7 +24,7 @@ public struct Subscription {
|
|||
name: String,
|
||||
type: SubscriptionType,
|
||||
folder: String,
|
||||
fetchContent: Bool,
|
||||
fetchContentType: FetchContentType,
|
||||
newsletterEmailAddress: String?,
|
||||
status: SubscriptionStatus,
|
||||
unsubscribeHttpUrl: String?,
|
||||
|
|
@ -39,7 +39,7 @@ public struct Subscription {
|
|||
self.name = name
|
||||
self.type = type
|
||||
self.folder = folder
|
||||
self.fetchContent = fetchContent
|
||||
self.fetchContentType = fetchContentType
|
||||
self.newsletterEmailAddress = newsletterEmailAddress
|
||||
self.status = status
|
||||
self.unsubscribeHttpUrl = unsubscribeHttpUrl
|
||||
|
|
@ -60,3 +60,9 @@ public enum SubscriptionType {
|
|||
case newsletter
|
||||
case feed
|
||||
}
|
||||
|
||||
public enum FetchContentType {
|
||||
case always
|
||||
case never
|
||||
case whenEmpty
|
||||
}
|
||||
|
|
|
|||
|
|
@ -667,6 +667,8 @@ extension Objects {
|
|||
let contentReader: [String: Enums.ContentReader]
|
||||
let createdAt: [String: DateTime]
|
||||
let description: [String: String]
|
||||
let directionality: [String: Enums.DirectionalityType]
|
||||
let feedContent: [String: String]
|
||||
let folder: [String: String]
|
||||
let hasContent: [String: Bool]
|
||||
let hash: [String: String]
|
||||
|
|
@ -742,6 +744,14 @@ extension Objects.Article: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "directionality":
|
||||
if let value = try container.decode(Enums.DirectionalityType?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "feedContent":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "folder":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -901,6 +911,8 @@ extension Objects.Article: Decodable {
|
|||
contentReader = map["contentReader"]
|
||||
createdAt = map["createdAt"]
|
||||
description = map["description"]
|
||||
directionality = map["directionality"]
|
||||
feedContent = map["feedContent"]
|
||||
folder = map["folder"]
|
||||
hasContent = map["hasContent"]
|
||||
hash = map["hash"]
|
||||
|
|
@ -1025,6 +1037,36 @@ extension Fields where TypeLock == Objects.Article {
|
|||
}
|
||||
}
|
||||
|
||||
func directionality() throws -> Enums.DirectionalityType? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "directionality",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.directionality[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func feedContent() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "feedContent",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.feedContent[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func folder() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "folder",
|
||||
|
|
@ -8127,6 +8169,7 @@ extension Objects {
|
|||
let quote: [String: String]
|
||||
let reactions: [String: [Objects.Reaction]]
|
||||
let replies: [String: [Objects.HighlightReply]]
|
||||
let representation: [String: Enums.RepresentationType]
|
||||
let sharedAt: [String: DateTime]
|
||||
let shortId: [String: String]
|
||||
let suffix: [String: String]
|
||||
|
|
@ -8208,6 +8251,10 @@ extension Objects.Highlight: Decodable {
|
|||
if let value = try container.decode([Objects.HighlightReply]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "representation":
|
||||
if let value = try container.decode(Enums.RepresentationType?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "sharedAt":
|
||||
if let value = try container.decode(DateTime?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -8256,6 +8303,7 @@ extension Objects.Highlight: Decodable {
|
|||
quote = map["quote"]
|
||||
reactions = map["reactions"]
|
||||
replies = map["replies"]
|
||||
representation = map["representation"]
|
||||
sharedAt = map["sharedAt"]
|
||||
shortId = map["shortId"]
|
||||
suffix = map["suffix"]
|
||||
|
|
@ -8494,6 +8542,24 @@ extension Fields where TypeLock == Objects.Highlight {
|
|||
}
|
||||
}
|
||||
|
||||
func representation() throws -> Enums.RepresentationType {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "representation",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.representation[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return Enums.RepresentationType.allCases.first!
|
||||
}
|
||||
}
|
||||
|
||||
func sharedAt() throws -> DateTime? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "sharedAt",
|
||||
|
|
@ -8985,6 +9051,7 @@ extension Objects {
|
|||
let enabled: [String: Bool]
|
||||
let id: [String: String]
|
||||
let name: [String: String]
|
||||
let settings: [String: String]
|
||||
let taskName: [String: String]
|
||||
let token: [String: String]
|
||||
let type: [String: Enums.IntegrationType]
|
||||
|
|
@ -9024,6 +9091,10 @@ extension Objects.Integration: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "settings":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "taskName":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -9054,6 +9125,7 @@ extension Objects.Integration: Decodable {
|
|||
enabled = map["enabled"]
|
||||
id = map["id"]
|
||||
name = map["name"]
|
||||
settings = map["settings"]
|
||||
taskName = map["taskName"]
|
||||
token = map["token"]
|
||||
type = map["type"]
|
||||
|
|
@ -9134,6 +9206,21 @@ extension Fields where TypeLock == Objects.Integration {
|
|||
}
|
||||
}
|
||||
|
||||
func settings() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "settings",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.settings[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func taskName() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "taskName",
|
||||
|
|
@ -18126,6 +18213,7 @@ extension Selection where TypeLock == Never, Type == Never {
|
|||
extension Objects {
|
||||
struct SearchItem {
|
||||
let __typename: TypeName = .searchItem
|
||||
let aiSummary: [String: String]
|
||||
let annotation: [String: String]
|
||||
let archivedAt: [String: DateTime]
|
||||
let author: [String: String]
|
||||
|
|
@ -18134,6 +18222,8 @@ extension Objects {
|
|||
let contentReader: [String: Enums.ContentReader]
|
||||
let createdAt: [String: DateTime]
|
||||
let description: [String: String]
|
||||
let directionality: [String: Enums.DirectionalityType]
|
||||
let feedContent: [String: String]
|
||||
let folder: [String: String]
|
||||
let highlights: [String: [Objects.Highlight]]
|
||||
let id: [String: String]
|
||||
|
|
@ -18146,7 +18236,6 @@ extension Objects {
|
|||
let ownedByViewer: [String: Bool]
|
||||
let pageId: [String: String]
|
||||
let pageType: [String: Enums.PageType]
|
||||
let previewContent: [String: String]
|
||||
let previewContentType: [String: String]
|
||||
let publishedAt: [String: DateTime]
|
||||
let quote: [String: String]
|
||||
|
|
@ -18188,6 +18277,10 @@ extension Objects.SearchItem: Decodable {
|
|||
let field = GraphQLField.getFieldNameFromAlias(alias)
|
||||
|
||||
switch field {
|
||||
case "aiSummary":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "annotation":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -18220,6 +18313,14 @@ extension Objects.SearchItem: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "directionality":
|
||||
if let value = try container.decode(Enums.DirectionalityType?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "feedContent":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "folder":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -18268,10 +18369,6 @@ extension Objects.SearchItem: Decodable {
|
|||
if let value = try container.decode(Enums.PageType?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "previewContent":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "previewContentType":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -18370,6 +18467,7 @@ extension Objects.SearchItem: Decodable {
|
|||
}
|
||||
}
|
||||
|
||||
aiSummary = map["aiSummary"]
|
||||
annotation = map["annotation"]
|
||||
archivedAt = map["archivedAt"]
|
||||
author = map["author"]
|
||||
|
|
@ -18378,6 +18476,8 @@ extension Objects.SearchItem: Decodable {
|
|||
contentReader = map["contentReader"]
|
||||
createdAt = map["createdAt"]
|
||||
description = map["description"]
|
||||
directionality = map["directionality"]
|
||||
feedContent = map["feedContent"]
|
||||
folder = map["folder"]
|
||||
highlights = map["highlights"]
|
||||
id = map["id"]
|
||||
|
|
@ -18390,7 +18490,6 @@ extension Objects.SearchItem: Decodable {
|
|||
ownedByViewer = map["ownedByViewer"]
|
||||
pageId = map["pageId"]
|
||||
pageType = map["pageType"]
|
||||
previewContent = map["previewContent"]
|
||||
previewContentType = map["previewContentType"]
|
||||
publishedAt = map["publishedAt"]
|
||||
quote = map["quote"]
|
||||
|
|
@ -18417,6 +18516,21 @@ extension Objects.SearchItem: Decodable {
|
|||
}
|
||||
|
||||
extension Fields where TypeLock == Objects.SearchItem {
|
||||
func aiSummary() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "aiSummary",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.aiSummary[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func annotation() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "annotation",
|
||||
|
|
@ -18543,6 +18657,36 @@ extension Fields where TypeLock == Objects.SearchItem {
|
|||
}
|
||||
}
|
||||
|
||||
func directionality() throws -> Enums.DirectionalityType? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "directionality",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.directionality[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func feedContent() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "feedContent",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.feedContent[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func folder() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "folder",
|
||||
|
|
@ -18737,21 +18881,6 @@ extension Fields where TypeLock == Objects.SearchItem {
|
|||
}
|
||||
}
|
||||
|
||||
func previewContent() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "previewContent",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.previewContent[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func previewContentType() throws -> String? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "previewContentType",
|
||||
|
|
@ -21284,6 +21413,7 @@ extension Objects {
|
|||
let description: [String: String]
|
||||
let failedAt: [String: DateTime]
|
||||
let fetchContent: [String: Bool]
|
||||
let fetchContentType: [String: Enums.FetchContentType]
|
||||
let folder: [String: String]
|
||||
let icon: [String: String]
|
||||
let id: [String: String]
|
||||
|
|
@ -21342,6 +21472,10 @@ extension Objects.Subscription: Decodable {
|
|||
if let value = try container.decode(Bool?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "fetchContentType":
|
||||
if let value = try container.decode(Enums.FetchContentType?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "folder":
|
||||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -21418,6 +21552,7 @@ extension Objects.Subscription: Decodable {
|
|||
description = map["description"]
|
||||
failedAt = map["failedAt"]
|
||||
fetchContent = map["fetchContent"]
|
||||
fetchContentType = map["fetchContentType"]
|
||||
folder = map["folder"]
|
||||
icon = map["icon"]
|
||||
id = map["id"]
|
||||
|
|
@ -21536,6 +21671,24 @@ extension Fields where TypeLock == Objects.Subscription {
|
|||
}
|
||||
}
|
||||
|
||||
func fetchContentType() throws -> Enums.FetchContentType {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "fetchContentType",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
if let data = data.fetchContentType[field.alias!] {
|
||||
return data
|
||||
}
|
||||
throw HttpError.badpayload
|
||||
case .mocking:
|
||||
return Enums.FetchContentType.allCases.first!
|
||||
}
|
||||
}
|
||||
|
||||
func folder() throws -> String {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "folder",
|
||||
|
|
@ -24692,6 +24845,7 @@ extension Objects {
|
|||
struct User {
|
||||
let __typename: TypeName = .user
|
||||
let email: [String: String]
|
||||
let features: [String: [String?]]
|
||||
let followersCount: [String: Int]
|
||||
let friendsCount: [String: Int]
|
||||
let id: [String: String]
|
||||
|
|
@ -24730,6 +24884,10 @@ extension Objects.User: Decodable {
|
|||
if let value = try container.decode(String?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "features":
|
||||
if let value = try container.decode([String?]?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
}
|
||||
case "followersCount":
|
||||
if let value = try container.decode(Int?.self, forKey: codingKey) {
|
||||
map.set(key: field, hash: alias, value: value as Any)
|
||||
|
|
@ -24801,6 +24959,7 @@ extension Objects.User: Decodable {
|
|||
}
|
||||
|
||||
email = map["email"]
|
||||
features = map["features"]
|
||||
followersCount = map["followersCount"]
|
||||
friendsCount = map["friendsCount"]
|
||||
id = map["id"]
|
||||
|
|
@ -24835,6 +24994,21 @@ extension Fields where TypeLock == Objects.User {
|
|||
}
|
||||
}
|
||||
|
||||
func features() throws -> [String?]? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "features",
|
||||
arguments: []
|
||||
)
|
||||
select(field)
|
||||
|
||||
switch response {
|
||||
case let .decoding(data):
|
||||
return data.features[field.alias!]
|
||||
case .mocking:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func followersCount() throws -> Int? {
|
||||
let field = GraphQLField.leaf(
|
||||
name: "followersCount",
|
||||
|
|
@ -34148,6 +34322,15 @@ extension Enums {
|
|||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// DirectionalityType
|
||||
enum DirectionalityType: String, CaseIterable, Codable {
|
||||
case ltr = "LTR"
|
||||
|
||||
case rtl = "RTL"
|
||||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// EmptyTrashErrorCode
|
||||
enum EmptyTrashErrorCode: String, CaseIterable, Codable {
|
||||
|
|
@ -34180,6 +34363,17 @@ extension Enums {
|
|||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// FetchContentType
|
||||
enum FetchContentType: String, CaseIterable, Codable {
|
||||
case always = "ALWAYS"
|
||||
|
||||
case never = "NEVER"
|
||||
|
||||
case whenEmpty = "WHEN_EMPTY"
|
||||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// FiltersErrorCode
|
||||
enum FiltersErrorCode: String, CaseIterable, Codable {
|
||||
|
|
@ -34521,6 +34715,15 @@ extension Enums {
|
|||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// RepresentationType
|
||||
enum RepresentationType: String, CaseIterable, Codable {
|
||||
case content = "CONTENT"
|
||||
|
||||
case feedContent = "FEED_CONTENT"
|
||||
}
|
||||
}
|
||||
|
||||
extension Enums {
|
||||
/// RevokeApiKeyErrorCode
|
||||
enum RevokeApiKeyErrorCode: String, CaseIterable, Codable {
|
||||
|
|
@ -34539,6 +34742,8 @@ extension Enums {
|
|||
|
||||
case archive = "ARCHIVE"
|
||||
|
||||
case delete = "DELETE"
|
||||
|
||||
case markAsRead = "MARK_AS_READ"
|
||||
|
||||
case sendNotification = "SEND_NOTIFICATION"
|
||||
|
|
@ -35306,6 +35511,8 @@ extension InputObjects {
|
|||
|
||||
var quote: OptionalArgument<String> = .absent()
|
||||
|
||||
var representation: OptionalArgument<Enums.RepresentationType> = .absent()
|
||||
|
||||
var sharedAt: OptionalArgument<DateTime> = .absent()
|
||||
|
||||
var shortId: String
|
||||
|
|
@ -35326,6 +35533,7 @@ extension InputObjects {
|
|||
if patch.hasValue { try container.encode(patch, forKey: .patch) }
|
||||
if prefix.hasValue { try container.encode(prefix, forKey: .prefix) }
|
||||
if quote.hasValue { try container.encode(quote, forKey: .quote) }
|
||||
if representation.hasValue { try container.encode(representation, forKey: .representation) }
|
||||
if sharedAt.hasValue { try container.encode(sharedAt, forKey: .sharedAt) }
|
||||
try container.encode(shortId, forKey: .shortId)
|
||||
if suffix.hasValue { try container.encode(suffix, forKey: .suffix) }
|
||||
|
|
@ -35343,6 +35551,7 @@ extension InputObjects {
|
|||
case patch
|
||||
case prefix
|
||||
case quote
|
||||
case representation
|
||||
case sharedAt
|
||||
case shortId
|
||||
case suffix
|
||||
|
|
@ -35602,6 +35811,8 @@ extension InputObjects {
|
|||
|
||||
var quote: String
|
||||
|
||||
var representation: OptionalArgument<Enums.RepresentationType> = .absent()
|
||||
|
||||
var shortId: String
|
||||
|
||||
var suffix: OptionalArgument<String> = .absent()
|
||||
|
|
@ -35619,6 +35830,7 @@ extension InputObjects {
|
|||
try container.encode(patch, forKey: .patch)
|
||||
if prefix.hasValue { try container.encode(prefix, forKey: .prefix) }
|
||||
try container.encode(quote, forKey: .quote)
|
||||
if representation.hasValue { try container.encode(representation, forKey: .representation) }
|
||||
try container.encode(shortId, forKey: .shortId)
|
||||
if suffix.hasValue { try container.encode(suffix, forKey: .suffix) }
|
||||
}
|
||||
|
|
@ -35635,6 +35847,7 @@ extension InputObjects {
|
|||
case patch
|
||||
case prefix
|
||||
case quote
|
||||
case representation
|
||||
case shortId
|
||||
case suffix
|
||||
}
|
||||
|
|
@ -36228,6 +36441,8 @@ extension InputObjects {
|
|||
|
||||
var name: String
|
||||
|
||||
var settings: OptionalArgument<String> = .absent()
|
||||
|
||||
var syncedAt: OptionalArgument<DateTime> = .absent()
|
||||
|
||||
var taskName: OptionalArgument<String> = .absent()
|
||||
|
|
@ -36242,6 +36457,7 @@ extension InputObjects {
|
|||
if id.hasValue { try container.encode(id, forKey: .id) }
|
||||
if importItemState.hasValue { try container.encode(importItemState, forKey: .importItemState) }
|
||||
try container.encode(name, forKey: .name)
|
||||
if settings.hasValue { try container.encode(settings, forKey: .settings) }
|
||||
if syncedAt.hasValue { try container.encode(syncedAt, forKey: .syncedAt) }
|
||||
if taskName.hasValue { try container.encode(taskName, forKey: .taskName) }
|
||||
try container.encode(token, forKey: .token)
|
||||
|
|
@ -36253,6 +36469,7 @@ extension InputObjects {
|
|||
case id
|
||||
case importItemState
|
||||
case name
|
||||
case settings
|
||||
case syncedAt
|
||||
case taskName
|
||||
case token
|
||||
|
|
@ -36511,6 +36728,8 @@ extension InputObjects {
|
|||
|
||||
var fetchContent: OptionalArgument<Bool> = .absent()
|
||||
|
||||
var fetchContentType: OptionalArgument<Enums.FetchContentType> = .absent()
|
||||
|
||||
var folder: OptionalArgument<String> = .absent()
|
||||
|
||||
var isPrivate: OptionalArgument<Bool> = .absent()
|
||||
|
|
@ -36523,6 +36742,7 @@ extension InputObjects {
|
|||
var container = encoder.container(keyedBy: CodingKeys.self)
|
||||
if autoAddToLibrary.hasValue { try container.encode(autoAddToLibrary, forKey: .autoAddToLibrary) }
|
||||
if fetchContent.hasValue { try container.encode(fetchContent, forKey: .fetchContent) }
|
||||
if fetchContentType.hasValue { try container.encode(fetchContentType, forKey: .fetchContentType) }
|
||||
if folder.hasValue { try container.encode(folder, forKey: .folder) }
|
||||
if isPrivate.hasValue { try container.encode(isPrivate, forKey: .isPrivate) }
|
||||
if subscriptionType.hasValue { try container.encode(subscriptionType, forKey: .subscriptionType) }
|
||||
|
|
@ -36532,6 +36752,7 @@ extension InputObjects {
|
|||
enum CodingKeys: String, CodingKey {
|
||||
case autoAddToLibrary
|
||||
case fetchContent
|
||||
case fetchContentType
|
||||
case folder
|
||||
case isPrivate
|
||||
case subscriptionType
|
||||
|
|
@ -36828,6 +37049,8 @@ extension InputObjects {
|
|||
|
||||
var fetchContent: OptionalArgument<Bool> = .absent()
|
||||
|
||||
var fetchContentType: OptionalArgument<Enums.FetchContentType> = .absent()
|
||||
|
||||
var folder: OptionalArgument<String> = .absent()
|
||||
|
||||
var id: String
|
||||
|
|
@ -36852,6 +37075,7 @@ extension InputObjects {
|
|||
if description.hasValue { try container.encode(description, forKey: .description) }
|
||||
if failedAt.hasValue { try container.encode(failedAt, forKey: .failedAt) }
|
||||
if fetchContent.hasValue { try container.encode(fetchContent, forKey: .fetchContent) }
|
||||
if fetchContentType.hasValue { try container.encode(fetchContentType, forKey: .fetchContentType) }
|
||||
if folder.hasValue { try container.encode(folder, forKey: .folder) }
|
||||
try container.encode(id, forKey: .id)
|
||||
if isPrivate.hasValue { try container.encode(isPrivate, forKey: .isPrivate) }
|
||||
|
|
@ -36868,6 +37092,7 @@ extension InputObjects {
|
|||
case description
|
||||
case failedAt
|
||||
case fetchContent
|
||||
case fetchContentType
|
||||
case folder
|
||||
case id
|
||||
case isPrivate
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ public struct Rule {
|
|||
public enum RuleActionType {
|
||||
case addLabel
|
||||
case archive
|
||||
case delete
|
||||
case markAsRead
|
||||
case sendNotification
|
||||
|
||||
|
|
@ -25,6 +26,8 @@ public enum RuleActionType {
|
|||
return .markAsRead
|
||||
case Enums.RuleActionType.sendNotification:
|
||||
return .sendNotification
|
||||
case .delete:
|
||||
return .delete
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
public extension DataService {
|
||||
func updateSubscription(_ subscriptionID: String, folder: String? = nil, fetchContent: Bool? = nil) async throws {
|
||||
func updateSubscription(_ subscriptionID: String, folder: String? = nil, fetchContentType: FetchContentType? = nil) async throws {
|
||||
enum MutationResult {
|
||||
case success(subscriptionID: String)
|
||||
case error(errorMessage: String)
|
||||
|
|
@ -20,7 +20,7 @@ public extension DataService {
|
|||
let mutation = Selection.Mutation {
|
||||
try $0.updateSubscription(
|
||||
input: InputObjects.UpdateSubscriptionInput(
|
||||
fetchContent: OptionalArgument(fetchContent),
|
||||
fetchContentType: OptionalArgument(fetchContentType?.toGQLType()),
|
||||
folder: OptionalArgument(folder),
|
||||
id: subscriptionID
|
||||
),
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ let subscriptionSelection = Selection.Subscription {
|
|||
name: try $0.name(),
|
||||
type: try SubscriptionType.from($0.type()),
|
||||
folder: try $0.folder(),
|
||||
fetchContent: try $0.fetchContent(),
|
||||
fetchContentType: try FetchContentType.from($0.fetchContentType()),
|
||||
newsletterEmailAddress: try $0.newsletterEmail(),
|
||||
status: try SubscriptionStatus.make(from: $0.status()),
|
||||
unsubscribeHttpUrl: try $0.unsubscribeHttpUrl(),
|
||||
|
|
@ -45,3 +45,26 @@ extension SubscriptionType {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension FetchContentType {
|
||||
static func from(_ other: Enums.FetchContentType) -> FetchContentType {
|
||||
switch other {
|
||||
case .always:
|
||||
return .always
|
||||
case .never:
|
||||
return .never
|
||||
case .whenEmpty:
|
||||
return .whenEmpty
|
||||
}
|
||||
}
|
||||
func toGQLType() -> Enums.FetchContentType {
|
||||
switch self {
|
||||
case .always:
|
||||
return .always
|
||||
case .never:
|
||||
return .never
|
||||
case .whenEmpty:
|
||||
return .whenEmpty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,7 +301,7 @@ public final class OmnivoreWebView: WKWebView {
|
|||
case Selector(("_lookup:")): return (currentMenu == .defaultMenu)
|
||||
case Selector(("_define:")): return (currentMenu == .defaultMenu)
|
||||
case Selector(("_translate:")): return (currentMenu == .defaultMenu)
|
||||
case Selector(("_findSelected:")): return (currentMenu == .defaultMenu)
|
||||
// case Selector(("_findSelected:")): return (currentMenu == .defaultMenu)
|
||||
default: return false
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,11 +12,13 @@ public enum GridCardAction {
|
|||
|
||||
public struct GridCard: View {
|
||||
@ObservedObject var item: Models.LibraryItem
|
||||
let savedAtStr: String
|
||||
|
||||
public init(
|
||||
item: Models.LibraryItem
|
||||
) {
|
||||
self.item = item
|
||||
self.savedAtStr = savedDateString(item.savedAt)
|
||||
}
|
||||
|
||||
var imageBox: some View {
|
||||
|
|
@ -198,6 +200,10 @@ public struct GridCard: View {
|
|||
$0.icon
|
||||
}
|
||||
|
||||
Text(savedAtStr)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
+
|
||||
Text("\(estimatedReadingTime)")
|
||||
.font(.caption2).fontWeight(.medium)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
|
|
|
|||
|
|
@ -44,14 +44,33 @@ public extension View {
|
|||
}
|
||||
}
|
||||
|
||||
func savedDateString(_ savedAt: Date?) -> String {
|
||||
if let savedAt = savedAt {
|
||||
let locale = Locale.current
|
||||
let dateFormatter = DateFormatter()
|
||||
if Calendar.current.isDateInToday(savedAt) {
|
||||
dateFormatter.dateStyle = .none
|
||||
dateFormatter.timeStyle = .short
|
||||
} else {
|
||||
dateFormatter.dateFormat = "MMM dd"
|
||||
}
|
||||
dateFormatter.locale = locale
|
||||
return dateFormatter.string(from: savedAt) + " • "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
public struct LibraryItemCard: View {
|
||||
let viewer: Viewer?
|
||||
@ObservedObject var item: Models.LibraryItem
|
||||
@State var noteLineLimit: Int? = 3
|
||||
|
||||
let savedAtStr: String
|
||||
|
||||
public init(item: Models.LibraryItem, viewer: Viewer?) {
|
||||
self.item = item
|
||||
self.viewer = viewer
|
||||
self.savedAtStr = savedDateString(item.savedAt)
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
|
|
@ -215,23 +234,28 @@ public struct LibraryItemCard: View {
|
|||
$0.icon
|
||||
}
|
||||
|
||||
Text(savedAtStr)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
|
||||
+
|
||||
Text("\(estimatedReadingTime)")
|
||||
.font(.caption2).fontWeight(.medium)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
|
||||
+
|
||||
Text("\(readingProgress)")
|
||||
.font(.caption2).fontWeight(.medium)
|
||||
.font(.footnote)
|
||||
.foregroundColor(isPartiallyRead ? Color.appGreenSuccess : Color.themeLibraryItemSubtle)
|
||||
|
||||
+
|
||||
Text("\(highlightsText)")
|
||||
.font(.caption2).fontWeight(.medium)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
|
||||
+
|
||||
Text("\(notesText)")
|
||||
.font(.caption2).fontWeight(.medium)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
|
@ -281,13 +305,13 @@ public struct LibraryItemCard: View {
|
|||
var byLine: some View {
|
||||
if let origin = cardSiteName(item.pageURLString) {
|
||||
Text(bylineStr + " | " + origin)
|
||||
.font(.caption2)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(1)
|
||||
} else {
|
||||
Text(bylineStr)
|
||||
.font(.caption2)
|
||||
.font(.footnote)
|
||||
.foregroundColor(Color.themeLibraryItemSubtle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.lineLimit(1)
|
||||
|
|
@ -295,7 +319,7 @@ public struct LibraryItemCard: View {
|
|||
}
|
||||
|
||||
public var articleInfo: some View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
VStack(alignment: .leading, spacing: 7) {
|
||||
readInfo
|
||||
.dynamicTypeSize(.xSmall ... .medium)
|
||||
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"@google-cloud/tasks": "^4.0.0",
|
||||
"@graphql-tools/utils": "^9.1.1",
|
||||
"@langchain/openai": "^0.0.14",
|
||||
"@notionhq/client": "^2.2.14",
|
||||
"@omnivore/content-handler": "1.0.0",
|
||||
"@omnivore/liqe": "1.0.0",
|
||||
"@omnivore/readability": "1.0.0",
|
||||
|
|
@ -45,6 +46,7 @@
|
|||
"@sentry/integrations": "^7.10.0",
|
||||
"@sentry/node": "^5.26.0",
|
||||
"@sentry/tracing": "^7.9.0",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"addressparser": "^1.0.1",
|
||||
"apollo-datasource": "^3.3.1",
|
||||
"apollo-server-express": "^3.6.3",
|
||||
|
|
@ -97,6 +99,7 @@
|
|||
"sanitize-html": "^2.3.2",
|
||||
"sax": "^1.3.0",
|
||||
"search-query-parser": "^1.6.0",
|
||||
"showdown": "^2.1.0",
|
||||
"snake-case": "^3.0.3",
|
||||
"supertest": "^6.2.2",
|
||||
"ts-loader": "^9.3.0",
|
||||
|
|
@ -107,7 +110,9 @@
|
|||
"uuid": "^8.3.1",
|
||||
"voca": "^1.4.0",
|
||||
"winston": "^3.3.3",
|
||||
"word-counting": "^1.1.4"
|
||||
"word-counting": "^1.1.4",
|
||||
"youtubei": "^1.3.4",
|
||||
"youtubei.js": "^9.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/register": "^7.14.5",
|
||||
|
|
@ -136,6 +141,7 @@
|
|||
"@types/private-ip": "^1.0.0",
|
||||
"@types/sanitize-html": "^1.27.1",
|
||||
"@types/sax": "^1.2.7",
|
||||
"@types/showdown": "^2.0.6",
|
||||
"@types/sinon": "^10.0.13",
|
||||
"@types/sinon-chai": "^3.2.8",
|
||||
"@types/supertest": "^2.0.11",
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ export class Highlight {
|
|||
createdAt!: Date
|
||||
|
||||
@UpdateDateColumn()
|
||||
updatedAt?: Date | null
|
||||
updatedAt!: Date
|
||||
|
||||
@Column('timestamp')
|
||||
sharedAt?: Date
|
||||
|
|
|
|||
|
|
@ -59,4 +59,7 @@ export class Integration {
|
|||
|
||||
@Column('enum', { enum: ImportItemState, nullable: true })
|
||||
importItemState?: ImportItemState | null
|
||||
|
||||
@Column('jsonb', { nullable: true })
|
||||
settings?: any
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1267,6 +1267,7 @@ export type Integration = {
|
|||
enabled: Scalars['Boolean'];
|
||||
id: Scalars['ID'];
|
||||
name: Scalars['String'];
|
||||
settings?: Maybe<Scalars['JSON']>;
|
||||
taskName?: Maybe<Scalars['String']>;
|
||||
token: Scalars['String'];
|
||||
type: IntegrationType;
|
||||
|
|
@ -2836,6 +2837,7 @@ export type SetIntegrationInput = {
|
|||
id?: InputMaybe<Scalars['ID']>;
|
||||
importItemState?: InputMaybe<ImportItemState>;
|
||||
name: Scalars['String'];
|
||||
settings?: InputMaybe<Scalars['JSON']>;
|
||||
syncedAt?: InputMaybe<Scalars['Date']>;
|
||||
taskName?: InputMaybe<Scalars['String']>;
|
||||
token: Scalars['String'];
|
||||
|
|
@ -5767,6 +5769,7 @@ export type IntegrationResolvers<ContextType = ResolverContext, ParentType exten
|
|||
enabled?: Resolver<ResolversTypes['Boolean'], ParentType, ContextType>;
|
||||
id?: Resolver<ResolversTypes['ID'], ParentType, ContextType>;
|
||||
name?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
settings?: Resolver<Maybe<ResolversTypes['JSON']>, ParentType, ContextType>;
|
||||
taskName?: Resolver<Maybe<ResolversTypes['String']>, ParentType, ContextType>;
|
||||
token?: Resolver<ResolversTypes['String'], ParentType, ContextType>;
|
||||
type?: Resolver<ResolversTypes['IntegrationType'], ParentType, ContextType>;
|
||||
|
|
|
|||
|
|
@ -1133,6 +1133,7 @@ type Integration {
|
|||
enabled: Boolean!
|
||||
id: ID!
|
||||
name: String!
|
||||
settings: JSON
|
||||
taskName: String
|
||||
token: String!
|
||||
type: IntegrationType!
|
||||
|
|
@ -2199,6 +2200,7 @@ input SetIntegrationInput {
|
|||
id: ID
|
||||
importItemState: ImportItemState
|
||||
name: String!
|
||||
settings: JSON
|
||||
syncedAt: Date
|
||||
taskName: String
|
||||
token: String!
|
||||
|
|
|
|||
|
|
@ -35,41 +35,54 @@ export const exportItem = async (jobData: ExportItemJobData) => {
|
|||
return
|
||||
}
|
||||
|
||||
// currently only readwise integration is supported
|
||||
const integration = integrations[0]
|
||||
await Promise.all(
|
||||
integrations.map(async (integration) => {
|
||||
try {
|
||||
const logObject = {
|
||||
userId,
|
||||
integrationId: integration.id,
|
||||
}
|
||||
logger.info('exporting item...', logObject)
|
||||
|
||||
const logObject = {
|
||||
userId,
|
||||
integrationId: integration.id,
|
||||
}
|
||||
logger.info('exporting item...', logObject)
|
||||
const client = getIntegrationClient(
|
||||
integration.name,
|
||||
integration.token,
|
||||
integration
|
||||
)
|
||||
|
||||
const client = getIntegrationClient(integration.name)
|
||||
const synced = await client.export(libraryItems)
|
||||
if (!synced) {
|
||||
logger.error('failed to export item', logObject)
|
||||
return false
|
||||
}
|
||||
|
||||
const synced = await client.export(integration.token, libraryItems)
|
||||
if (!synced) {
|
||||
logger.error('failed to export item', logObject)
|
||||
return false
|
||||
}
|
||||
const syncedAt = new Date()
|
||||
logger.info('updating integration...', {
|
||||
...logObject,
|
||||
syncedAt,
|
||||
})
|
||||
|
||||
const syncedAt = new Date()
|
||||
logger.info('updating integration...', {
|
||||
...logObject,
|
||||
syncedAt,
|
||||
})
|
||||
|
||||
// update integration syncedAt if successful
|
||||
const updated = await updateIntegration(
|
||||
integration.id,
|
||||
{
|
||||
syncedAt,
|
||||
},
|
||||
userId
|
||||
// update integration syncedAt if successful
|
||||
const updated = await updateIntegration(
|
||||
integration.id,
|
||||
{
|
||||
syncedAt,
|
||||
},
|
||||
userId
|
||||
)
|
||||
logger.info('integration updated', {
|
||||
...logObject,
|
||||
updated,
|
||||
})
|
||||
} catch (error) {
|
||||
logger.error('failed to export item', {
|
||||
userId,
|
||||
integrationId: integration.id,
|
||||
error,
|
||||
})
|
||||
}
|
||||
})
|
||||
)
|
||||
logger.info('integration updated', {
|
||||
...logObject,
|
||||
updated,
|
||||
})
|
||||
|
||||
return true
|
||||
}
|
||||
|
|
|
|||
521
packages/api/src/jobs/process-youtube-video.ts
Normal file
521
packages/api/src/jobs/process-youtube-video.ts
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
import { logger } from '../utils/logger'
|
||||
import { authTrx } from '../repository'
|
||||
import { libraryItemRepository } from '../repository/library_item'
|
||||
import { LibraryItem, LibraryItemState } from '../entity/library_item'
|
||||
|
||||
import { Chapter, Client as YouTubeClient } from 'youtubei'
|
||||
import showdown from 'showdown'
|
||||
import { parseHTML } from 'linkedom'
|
||||
import { parsePreparedContent } from '../utils/parser'
|
||||
import { OpenAI } from '@langchain/openai'
|
||||
import { PromptTemplate } from '@langchain/core/prompts'
|
||||
import { enqueueProcessYouTubeTranscript } from '../utils/createTask'
|
||||
import { env } from '../env'
|
||||
import * as stream from 'stream'
|
||||
|
||||
import { Storage } from '@google-cloud/storage'
|
||||
import { stringToHash } from '../utils/helpers'
|
||||
import { FeatureName, findFeatureByName } from '../services/features'
|
||||
|
||||
export interface ProcessYouTubeVideoJobData {
|
||||
userId: string
|
||||
libraryItemId: string
|
||||
}
|
||||
|
||||
export const PROCESS_YOUTUBE_VIDEO_JOB_NAME = 'process-youtube-video'
|
||||
export const PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME = 'process-youtube-transcript'
|
||||
|
||||
const TRANSCRIPT_PLACEHOLDER_TEXT =
|
||||
'* Omnivore is preparing a transcript for this video'
|
||||
|
||||
const calculateWordCount = (durationInSeconds: number): number => {
|
||||
// Calculate word count using the formula: word count = read time (in seconds) * words per second
|
||||
// Assuming average reading speed is 235 words per minute (or about 3.92 words per second)
|
||||
const wordsPerSecond = 3.92
|
||||
const wordCount = Math.round(durationInSeconds * wordsPerSecond)
|
||||
return wordCount
|
||||
}
|
||||
|
||||
interface ChapterProperties {
|
||||
title: string
|
||||
start: number
|
||||
}
|
||||
|
||||
interface TranscriptProperties {
|
||||
text: string
|
||||
start: number
|
||||
duration: number
|
||||
}
|
||||
|
||||
export const addTranscriptChapters = (
|
||||
chapters: ChapterProperties[],
|
||||
transcript: TranscriptProperties[]
|
||||
): TranscriptProperties[] => {
|
||||
chapters.sort((a, b) => a.start - b.start)
|
||||
|
||||
for (const chapter of chapters) {
|
||||
const startOffset = chapter.start
|
||||
const title = '\n\n## ' + chapter.title + '\n\n'
|
||||
|
||||
const index = transcript.findIndex(
|
||||
(textItem) => textItem.start > startOffset
|
||||
)
|
||||
|
||||
if (index !== -1) {
|
||||
transcript.splice(index, 0, {
|
||||
text: title,
|
||||
duration: 1,
|
||||
start: startOffset,
|
||||
})
|
||||
} else {
|
||||
transcript.push({ text: title, duration: 0, start: startOffset })
|
||||
}
|
||||
}
|
||||
return transcript
|
||||
}
|
||||
|
||||
const createTranscriptHash = (transcript: TranscriptProperties[]): string => {
|
||||
const rawTranscript = transcript.map((item) => item.text).join(' ')
|
||||
return stringToHash(rawTranscript)
|
||||
}
|
||||
|
||||
export const createTranscriptHTML = async (
|
||||
videoId: string,
|
||||
transcript: TranscriptProperties[]
|
||||
): Promise<string> => {
|
||||
let transcriptMarkdown = ''
|
||||
const transcriptHash = createTranscriptHash(transcript)
|
||||
const promptHash = stringToHash(process.env.YOUTUBE_TRANSCRIPT_PROMPT ?? '')
|
||||
|
||||
if (process.env.YOUTUBE_TRANSCRIPT_PROMPT && process.env.OPENAI_API_KEY) {
|
||||
const cachedTranscriptHTML = await fetchCachedYouTubeTranscript(
|
||||
videoId,
|
||||
transcriptHash,
|
||||
promptHash
|
||||
)
|
||||
if (cachedTranscriptHTML) {
|
||||
return cachedTranscriptHTML
|
||||
}
|
||||
|
||||
const llm = new OpenAI({
|
||||
modelName: 'gpt-4',
|
||||
configuration: {
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
},
|
||||
})
|
||||
const promptTemplate = PromptTemplate.fromTemplate(
|
||||
`${process.env.YOUTUBE_TRANSCRIPT_PROMPT}
|
||||
|
||||
{transcriptData}`
|
||||
)
|
||||
const chain = promptTemplate.pipe(llm)
|
||||
|
||||
let transcriptChunkLength = 0
|
||||
let transcriptChunk: TranscriptProperties[] = []
|
||||
for (const item of transcript) {
|
||||
if (transcriptChunkLength + item.text.length > 8000) {
|
||||
const result = await chain.invoke({
|
||||
transcriptData: transcriptChunk.map((item) => item.text).join(' '),
|
||||
})
|
||||
|
||||
transcriptMarkdown += result
|
||||
|
||||
transcriptChunk = []
|
||||
transcriptChunkLength = 0
|
||||
}
|
||||
|
||||
transcriptChunk.push(item)
|
||||
transcriptChunkLength += item.text.length
|
||||
}
|
||||
|
||||
if (transcriptChunk.length > 0) {
|
||||
const result = await chain.invoke({
|
||||
transcriptData: transcriptChunk.map((item) => item.text).join(' '),
|
||||
})
|
||||
|
||||
transcriptMarkdown += result
|
||||
}
|
||||
}
|
||||
|
||||
// If the LLM didn't give us enough data fallback to the raw template
|
||||
if (transcriptMarkdown.length < 1) {
|
||||
transcriptMarkdown = transcript.map((item) => item.text).join(' ')
|
||||
}
|
||||
|
||||
const converter = new showdown.Converter({
|
||||
backslashEscapesHTMLTags: true,
|
||||
})
|
||||
const transcriptHTML = converter.makeHtml(transcriptMarkdown)
|
||||
|
||||
if (process.env.YOUTUBE_TRANSCRIPT_PROMPT && process.env.OPENAI_API_KEY) {
|
||||
await cacheYouTubeTranscript(
|
||||
videoId,
|
||||
transcriptHash,
|
||||
promptHash,
|
||||
transcriptHTML
|
||||
)
|
||||
}
|
||||
|
||||
return transcriptHTML
|
||||
}
|
||||
|
||||
export const addTranscriptToReadableContent = async (
|
||||
originalUrl: string,
|
||||
originalHTML: string,
|
||||
transcriptHTML: string
|
||||
): Promise<string | undefined> => {
|
||||
const html = parseHTML(originalHTML)
|
||||
|
||||
const transcriptNode = html.document.querySelector(
|
||||
'#_omnivore_youtube_transcript'
|
||||
)
|
||||
|
||||
if (transcriptNode) {
|
||||
transcriptNode.innerHTML = transcriptHTML
|
||||
} else {
|
||||
const div = html.document.createElement('div')
|
||||
div.innerHTML = transcriptHTML
|
||||
html.document.body.appendChild(div)
|
||||
}
|
||||
|
||||
const preparedDocument = {
|
||||
document: html.document.toString(),
|
||||
pageInfo: {},
|
||||
}
|
||||
const updatedContent = await parsePreparedContent(
|
||||
originalUrl,
|
||||
preparedDocument,
|
||||
true
|
||||
)
|
||||
return updatedContent.parsedContent?.content
|
||||
}
|
||||
|
||||
export const addTranscriptPlaceholdReadableContent = async (
|
||||
originalUrl: string,
|
||||
originalHTML: string
|
||||
): Promise<string | undefined> => {
|
||||
const html = parseHTML(originalHTML)
|
||||
|
||||
const transcriptNode = html.document.querySelector(
|
||||
'#_omnivore_youtube_transcript'
|
||||
)
|
||||
|
||||
if (transcriptNode) {
|
||||
transcriptNode.innerHTML = TRANSCRIPT_PLACEHOLDER_TEXT
|
||||
} else {
|
||||
const div = html.document.createElement('div')
|
||||
div.innerHTML = TRANSCRIPT_PLACEHOLDER_TEXT
|
||||
html.document.body.appendChild(div)
|
||||
}
|
||||
|
||||
const preparedDocument = {
|
||||
document: html.document.toString(),
|
||||
pageInfo: {},
|
||||
}
|
||||
const updatedContent = await parsePreparedContent(
|
||||
originalUrl,
|
||||
preparedDocument,
|
||||
true
|
||||
)
|
||||
return updatedContent.parsedContent?.content
|
||||
}
|
||||
|
||||
async function readStringFromStorage(
|
||||
bucketName: string,
|
||||
fileName: string
|
||||
): Promise<string> {
|
||||
try {
|
||||
const storage = env.fileUpload?.gcsUploadSAKeyFilePath
|
||||
? new Storage({ keyFilename: env.fileUpload.gcsUploadSAKeyFilePath })
|
||||
: new Storage()
|
||||
|
||||
const existsResponse = await storage
|
||||
.bucket(bucketName)
|
||||
.file(fileName)
|
||||
.exists()
|
||||
const exists = existsResponse[0]
|
||||
|
||||
if (!exists) {
|
||||
throw new Error(
|
||||
`File '${fileName}' does not exist in bucket '${bucketName}'.`
|
||||
)
|
||||
}
|
||||
|
||||
// Download the file contents as a string
|
||||
const fileContentResponse = await storage
|
||||
.bucket(bucketName)
|
||||
.file(fileName)
|
||||
.download()
|
||||
const fileContent = fileContentResponse[0].toString()
|
||||
|
||||
console.log(`File '${fileName}' downloaded successfully as string.`)
|
||||
return fileContent
|
||||
} catch (error) {
|
||||
console.error('Error downloading file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const writeStringToStorage = async (
|
||||
bucketName: string,
|
||||
fileName: string,
|
||||
content: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const storage = env.fileUpload?.gcsUploadSAKeyFilePath
|
||||
? new Storage({ keyFilename: env.fileUpload.gcsUploadSAKeyFilePath })
|
||||
: new Storage()
|
||||
|
||||
const writableStream = storage
|
||||
.bucket(bucketName)
|
||||
.file(fileName)
|
||||
.createWriteStream()
|
||||
|
||||
// Convert the string content to a readable stream
|
||||
const readableStream = new stream.Readable()
|
||||
readableStream.push(content)
|
||||
readableStream.push(null) // Signal the end of the stream
|
||||
|
||||
// Pipe the readable stream to the writable stream to upload the file content
|
||||
await new Promise((resolve, reject) => {
|
||||
readableStream
|
||||
.pipe(writableStream)
|
||||
.on('finish', resolve)
|
||||
.on('error', reject)
|
||||
})
|
||||
|
||||
console.log(
|
||||
`File '${fileName}' uploaded successfully to bucket '${bucketName}'.`
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error uploading file:', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const fetchCachedYouTubeTranscript = async (
|
||||
videoId: string,
|
||||
transcriptHash: string,
|
||||
promptHash: string
|
||||
): Promise<string | undefined> => {
|
||||
const bucketName = env.fileUpload.gcsUploadBucket
|
||||
|
||||
try {
|
||||
return await readStringFromStorage(
|
||||
bucketName,
|
||||
`youtube-transcripts/${videoId}/${transcriptHash}.${promptHash}.html`
|
||||
)
|
||||
} catch (err) {
|
||||
logger.info(`unable to fetch cached transcript`, { error: err })
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const cacheYouTubeTranscript = async (
|
||||
videoId: string,
|
||||
transcriptHash: string,
|
||||
promptHash: string,
|
||||
transcript: string
|
||||
): Promise<void> => {
|
||||
const bucketName = env.fileUpload.gcsUploadBucket
|
||||
|
||||
try {
|
||||
await writeStringToStorage(
|
||||
bucketName,
|
||||
`youtube-transcripts/${videoId}/${transcriptHash}.${promptHash}.html`,
|
||||
transcript
|
||||
)
|
||||
} catch (err) {
|
||||
logger.info(`unable to cache transcript`, { error: err })
|
||||
}
|
||||
}
|
||||
|
||||
export const processYouTubeVideo = async (
|
||||
jobData: ProcessYouTubeVideoJobData
|
||||
) => {
|
||||
try {
|
||||
const libraryItem = await authTrx(
|
||||
async (tx) =>
|
||||
tx
|
||||
.withRepository(libraryItemRepository)
|
||||
.findById(jobData.libraryItemId),
|
||||
undefined,
|
||||
jobData.userId
|
||||
)
|
||||
if (
|
||||
!libraryItem ||
|
||||
libraryItem.state !== LibraryItemState.Succeeded ||
|
||||
!libraryItem.originalContent
|
||||
) {
|
||||
logger.info(
|
||||
`Not ready to get YouTube metadata job state: ${
|
||||
libraryItem?.state ?? 'null'
|
||||
}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const u = new URL(libraryItem.originalUrl)
|
||||
const videoId = u.searchParams.get('v')
|
||||
|
||||
if (!videoId) {
|
||||
console.warn('no video id for supplied youtube url', {
|
||||
url: libraryItem.originalUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let needsUpdate = false
|
||||
const youtube = new YouTubeClient()
|
||||
const video = await youtube.getVideo(videoId)
|
||||
if (!video) {
|
||||
console.warn('no video found for youtube url', {
|
||||
url: libraryItem.originalUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (video.description && libraryItem.description !== video.description) {
|
||||
needsUpdate = true
|
||||
libraryItem.description = video.description
|
||||
}
|
||||
|
||||
let duration = -1
|
||||
if ('duration' in video && video.duration > 0) {
|
||||
needsUpdate = true
|
||||
libraryItem.wordCount = calculateWordCount(video.duration)
|
||||
duration = video.duration
|
||||
}
|
||||
|
||||
if (
|
||||
await findFeatureByName(FeatureName.YouTubeTranscripts, jobData.userId)
|
||||
) {
|
||||
if ('getTranscript' in video && duration > 0 && duration < 1801) {
|
||||
// If the video has a transcript available, put a placehold in and
|
||||
// enqueue a job to process the full transcript
|
||||
const updatedContent = await addTranscriptPlaceholdReadableContent(
|
||||
libraryItem.originalUrl,
|
||||
libraryItem.originalContent
|
||||
)
|
||||
|
||||
if (updatedContent) {
|
||||
needsUpdate = true
|
||||
libraryItem.readableContent = updatedContent
|
||||
}
|
||||
|
||||
await enqueueProcessYouTubeTranscript({
|
||||
videoId,
|
||||
...jobData,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
const updated = await authTrx(
|
||||
async (t) => {
|
||||
return t
|
||||
.getRepository(LibraryItem)
|
||||
.update(jobData.libraryItemId, libraryItem)
|
||||
},
|
||||
undefined,
|
||||
jobData.userId
|
||||
)
|
||||
if (!updated) {
|
||||
console.warn('could not updated library item')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('error creating summary: ', err)
|
||||
}
|
||||
}
|
||||
|
||||
export interface ProcessYouTubeTranscriptJobData {
|
||||
userId: string
|
||||
videoId: string
|
||||
libraryItemId: string
|
||||
}
|
||||
|
||||
export const processYouTubeTranscript = async (
|
||||
jobData: ProcessYouTubeTranscriptJobData
|
||||
) => {
|
||||
try {
|
||||
const libraryItem = await authTrx(
|
||||
async (tx) =>
|
||||
tx
|
||||
.withRepository(libraryItemRepository)
|
||||
.findById(jobData.libraryItemId),
|
||||
undefined,
|
||||
jobData.userId
|
||||
)
|
||||
if (
|
||||
!libraryItem ||
|
||||
libraryItem.state !== LibraryItemState.Succeeded ||
|
||||
!libraryItem.originalContent
|
||||
) {
|
||||
logger.info(
|
||||
`Not ready to get YouTube metadata job state: ${
|
||||
libraryItem?.state ?? 'null'
|
||||
}`
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
let needsUpdate = false
|
||||
const youtube = new YouTubeClient()
|
||||
const video = await youtube.getVideo(jobData.videoId)
|
||||
if (!video) {
|
||||
logger.warn('no video found for youtube url', {
|
||||
url: libraryItem.originalUrl,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
let chapters: Chapter[] = []
|
||||
if ('chapters' in video) {
|
||||
chapters = video.chapters
|
||||
}
|
||||
|
||||
let transcript: TranscriptProperties[] | undefined = undefined
|
||||
if ('getTranscript' in video) {
|
||||
transcript = await video.getTranscript()
|
||||
}
|
||||
|
||||
if (transcript) {
|
||||
if (chapters) {
|
||||
transcript = addTranscriptChapters(chapters, transcript)
|
||||
}
|
||||
const transcriptHTML = await createTranscriptHTML(
|
||||
jobData.videoId,
|
||||
transcript
|
||||
)
|
||||
const updatedContent = await addTranscriptToReadableContent(
|
||||
libraryItem.originalUrl,
|
||||
libraryItem.originalContent,
|
||||
transcriptHTML
|
||||
)
|
||||
|
||||
if (updatedContent) {
|
||||
needsUpdate = true
|
||||
libraryItem.readableContent = updatedContent
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
const updated = await authTrx(
|
||||
async (t) => {
|
||||
return t
|
||||
.getRepository(LibraryItem)
|
||||
.update(jobData.libraryItemId, libraryItem)
|
||||
},
|
||||
undefined,
|
||||
jobData.userId
|
||||
)
|
||||
if (!updated) {
|
||||
console.warn('could not updated library item')
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('error creating summary: ', err)
|
||||
}
|
||||
}
|
||||
|
|
@ -43,7 +43,7 @@ export const refreshAllFeeds = async (db: DataSource): Promise<boolean> => {
|
|||
AND (s.scheduled_at <= NOW() OR s.scheduled_at IS NULL)
|
||||
AND u.status = $4
|
||||
GROUP BY
|
||||
s.url
|
||||
url
|
||||
`,
|
||||
['RSS', 'ACTIVE', 'following', 'ACTIVE']
|
||||
)) as RssSubscriptionGroup[]
|
||||
|
|
@ -76,7 +76,10 @@ const updateSubscriptionGroup = async (
|
|||
refreshContext: RSSRefreshContext
|
||||
) => {
|
||||
let feedURL = group.url
|
||||
const userList = JSON.stringify(group.userIds.sort())
|
||||
const userIds = group.userIds
|
||||
// sort the user ids so that the job id is consistent
|
||||
// [...userIds] creates a shallow copy, so sort() does not mutate the original
|
||||
const userList = JSON.stringify([...userIds].sort())
|
||||
if (!feedURL) {
|
||||
logger.error('no url for feed group', group)
|
||||
return
|
||||
|
|
@ -105,7 +108,7 @@ const updateSubscriptionGroup = async (
|
|||
scheduledTimestamps: group.scheduledDates.map((timestamp) =>
|
||||
timestamp.getTime()
|
||||
), // unix timestamp in milliseconds
|
||||
userIds: group.userIds,
|
||||
userIds,
|
||||
fetchContentTypes: group.fetchContentTypes,
|
||||
folders: group.folders,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Merge } from './util'
|
|||
import {
|
||||
enqueueAISummarizeJob,
|
||||
enqueueExportItem,
|
||||
enqueueProcessYouTubeVideo,
|
||||
enqueueTriggerRuleJob,
|
||||
enqueueWebhookJob,
|
||||
} from './utils/createTask'
|
||||
|
|
@ -17,6 +18,7 @@ import {
|
|||
findFeatureByName,
|
||||
getFeatureName,
|
||||
} from './services/features'
|
||||
import { processYouTubeVideo } from './jobs/process-youtube-video'
|
||||
|
||||
const logger = buildLogger('pubsub')
|
||||
|
||||
|
|
@ -27,6 +29,18 @@ type EntityData<T extends Record<string, any>> = Merge<
|
|||
{ libraryItemId: string }
|
||||
>
|
||||
|
||||
const isYouTubeVideoURL = (url: string | undefined): boolean => {
|
||||
if (!url) {
|
||||
return false
|
||||
}
|
||||
const u = new URL(url)
|
||||
if (!u.host.endsWith('youtube.com') && !u.host.endsWith('youtu.be')) {
|
||||
return false
|
||||
}
|
||||
const videoId = u.searchParams.get('v')
|
||||
return videoId != null
|
||||
}
|
||||
|
||||
export const createPubSubClient = (): PubsubClient => {
|
||||
const fieldsToDelete = ['user'] as const
|
||||
|
||||
|
|
@ -92,7 +106,17 @@ export const createPubSubClient = (): PubsubClient => {
|
|||
})
|
||||
|
||||
if (await findFeatureByName(FeatureName.AISummaries, userId)) {
|
||||
await enqueueAISummarizeJob({
|
||||
// await enqueueAISummarizeJob({
|
||||
// userId,
|
||||
// libraryItemId,
|
||||
// })
|
||||
}
|
||||
|
||||
if (
|
||||
'originalUrl' in data &&
|
||||
isYouTubeVideoURL(data['originalUrl'] as string | undefined)
|
||||
) {
|
||||
await enqueueProcessYouTubeVideo({
|
||||
userId,
|
||||
libraryItemId,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ import { redisDataSource } from './redis_data_source'
|
|||
import { CACHED_READING_POSITION_PREFIX } from './services/cached_reading_position'
|
||||
import { getJobPriority } from './utils/createTask'
|
||||
import { logger } from './utils/logger'
|
||||
import {
|
||||
PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME,
|
||||
PROCESS_YOUTUBE_VIDEO_JOB_NAME,
|
||||
processYouTubeTranscript,
|
||||
processYouTubeVideo,
|
||||
} from './jobs/process-youtube-video'
|
||||
|
||||
export const QUEUE_NAME = 'omnivore-backend-queue'
|
||||
export const JOB_VERSION = 'v001'
|
||||
|
|
@ -116,8 +122,14 @@ export const createWorker = (connection: ConnectionOptions) =>
|
|||
return exportItem(job.data)
|
||||
case AI_SUMMARIZE_JOB_NAME:
|
||||
return aiSummarize(job.data)
|
||||
case PROCESS_YOUTUBE_VIDEO_JOB_NAME:
|
||||
return processYouTubeVideo(job.data)
|
||||
case PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME:
|
||||
return processYouTubeTranscript(job.data)
|
||||
case EXPORT_ALL_ITEMS_JOB_NAME:
|
||||
return exportAllItems(job.data)
|
||||
default:
|
||||
logger.warn(`[queue-processor] unhandled job: ${job.name}`)
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -58,6 +58,8 @@ export const libraryItemRepository = appDataSource
|
|||
},
|
||||
|
||||
createByPopularRead(name: string, userId: string) {
|
||||
// set read_at to now and reading_progress_bottom_percent to 2
|
||||
// so the items show up in continue reading section
|
||||
return this.query(
|
||||
`
|
||||
INSERT INTO omnivore.library_item (
|
||||
|
|
@ -73,7 +75,9 @@ export const libraryItemRepository = appDataSource
|
|||
published_at,
|
||||
site_name,
|
||||
user_id,
|
||||
word_count
|
||||
word_count,
|
||||
read_at,
|
||||
reading_progress_bottom_percent
|
||||
)
|
||||
SELECT
|
||||
slug,
|
||||
|
|
@ -88,7 +92,9 @@ export const libraryItemRepository = appDataSource
|
|||
published_at,
|
||||
site_name,
|
||||
$2,
|
||||
word_count
|
||||
word_count,
|
||||
NOW(),
|
||||
2
|
||||
FROM
|
||||
omnivore.popular_read
|
||||
WHERE
|
||||
|
|
|
|||
|
|
@ -34,7 +34,6 @@ import {
|
|||
import { analytics } from '../../utils/analytics'
|
||||
import {
|
||||
deleteTask,
|
||||
enqueueExportAllItems,
|
||||
enqueueImportFromIntegration,
|
||||
} from '../../utils/createTask'
|
||||
import { authorized } from '../../utils/gql-utils'
|
||||
|
|
@ -55,6 +54,8 @@ export const setIntegrationResolver = authorized<
|
|||
input.type === IntegrationType.Import
|
||||
? input.importItemState || ImportItemState.Unarchived // default to unarchived
|
||||
: undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
settings: input.settings,
|
||||
}
|
||||
if (input.id) {
|
||||
// Update
|
||||
|
|
@ -69,9 +70,9 @@ export const setIntegrationResolver = authorized<
|
|||
integrationToSave.taskName = existingIntegration.taskName
|
||||
} else {
|
||||
// Create
|
||||
const integrationService = getIntegrationClient(input.name)
|
||||
const integrationService = getIntegrationClient(input.name, input.token)
|
||||
// authorize and get access token
|
||||
const token = await integrationService.accessToken(input.token)
|
||||
const token = await integrationService.accessToken()
|
||||
if (!token) {
|
||||
return {
|
||||
errorCodes: [SetIntegrationErrorCode.InvalidToken],
|
||||
|
|
@ -83,40 +84,6 @@ export const setIntegrationResolver = authorized<
|
|||
// save integration
|
||||
const integration = await saveIntegration(integrationToSave, uid)
|
||||
|
||||
if (integrationToSave.type === IntegrationType.Export && !input.id) {
|
||||
const authToken = await createIntegrationToken({
|
||||
uid,
|
||||
token: integration.token,
|
||||
})
|
||||
if (!authToken) {
|
||||
log.error('failed to create auth token', {
|
||||
integrationId: integration.id,
|
||||
})
|
||||
return {
|
||||
errorCodes: [SetIntegrationErrorCode.BadRequest],
|
||||
}
|
||||
}
|
||||
|
||||
// create a task to sync all the pages if new integration or enable integration (export type)
|
||||
await enqueueExportAllItems(integration.id, uid)
|
||||
} else if (integrationToSave.taskName) {
|
||||
// delete the task if disable integration and task exists
|
||||
const result = await deleteTask(integrationToSave.taskName)
|
||||
if (result) {
|
||||
log.info('task deleted', integrationToSave.taskName)
|
||||
}
|
||||
|
||||
// update task name in integration
|
||||
await updateIntegration(
|
||||
integration.id,
|
||||
{
|
||||
taskName: null,
|
||||
},
|
||||
uid
|
||||
)
|
||||
integration.taskName = null
|
||||
}
|
||||
|
||||
analytics.capture({
|
||||
distinctId: uid,
|
||||
event: 'integration_set',
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import axios from 'axios'
|
|||
import cors from 'cors'
|
||||
import express from 'express'
|
||||
import { env } from '../env'
|
||||
import { getIntegrationClient } from '../services/integrations'
|
||||
import { getClaimsByToken } from '../utils/auth'
|
||||
import { corsConfig } from '../utils/corsConfig'
|
||||
import { logger } from '../utils/logger'
|
||||
|
|
@ -10,10 +11,9 @@ export function integrationRouter() {
|
|||
const router = express.Router()
|
||||
// request token from pocket
|
||||
router.post(
|
||||
'/pocket/auth',
|
||||
'/:name/auth',
|
||||
cors<express.Request>(corsConfig),
|
||||
async (req: express.Request, res: express.Response) => {
|
||||
logger.info('pocket/request-token')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const token = (req.cookies.auth as string) || req.headers.authorization
|
||||
const claims = await getClaimsByToken(token)
|
||||
|
|
@ -21,37 +21,19 @@ export function integrationRouter() {
|
|||
return res.status(401).send('UNAUTHORIZED')
|
||||
}
|
||||
|
||||
const consumerKey = env.pocket.consumerKey
|
||||
const redirectUri = `${env.client.url}/settings/integrations`
|
||||
const integrationClient = getIntegrationClient(req.params.name, '')
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
const state = req.body.state as string
|
||||
try {
|
||||
// make a POST request to Pocket to get a request token
|
||||
const response = await axios.post<{ code: string }>(
|
||||
'https://getpocket.com/v3/oauth/request',
|
||||
{
|
||||
consumer_key: consumerKey,
|
||||
redirect_uri: redirectUri,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Accept': 'application/json',
|
||||
},
|
||||
}
|
||||
)
|
||||
const { code } = response.data
|
||||
const redirectUri = await integrationClient.auth(state)
|
||||
// redirect the user to Pocket to authorize the request token
|
||||
res.redirect(
|
||||
`https://getpocket.com/auth/authorize?request_token=${code}&redirect_uri=${redirectUri}${encodeURIComponent(
|
||||
`?pocketToken=${code}&state=${state}`
|
||||
)}`
|
||||
)
|
||||
res.redirect(redirectUri)
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
} else {
|
||||
logger.error('pocket/request-token exception:', error)
|
||||
logger.error(error)
|
||||
}
|
||||
|
||||
res.redirect(
|
||||
|
|
|
|||
|
|
@ -2015,6 +2015,7 @@ const schema = gql`
|
|||
createdAt: Date!
|
||||
updatedAt: Date
|
||||
taskName: String
|
||||
settings: JSON
|
||||
}
|
||||
|
||||
enum IntegrationType {
|
||||
|
|
@ -2050,6 +2051,7 @@ const schema = gql`
|
|||
syncedAt: Date
|
||||
importItemState: ImportItemState
|
||||
taskName: String
|
||||
settings: JSON
|
||||
}
|
||||
|
||||
union IntegrationsResult = IntegrationsSuccess | IntegrationsError
|
||||
|
|
|
|||
|
|
@ -6,8 +6,12 @@ import { env } from '../env'
|
|||
import { getRepository } from '../repository'
|
||||
import { logger } from '../utils/logger'
|
||||
|
||||
const MAX_ULTRA_REALISTIC_USERS = 1500
|
||||
const MAX_YOUTUBE_TRANSCRIPT_USERS = 100
|
||||
|
||||
export enum FeatureName {
|
||||
AISummaries = 'ai-summaries',
|
||||
YouTubeTranscripts = 'youtube-transcripts',
|
||||
UltraRealisticVoice = 'ultra-realistic-voice',
|
||||
}
|
||||
|
||||
|
|
@ -19,16 +23,31 @@ export const optInFeature = async (
|
|||
name: FeatureName,
|
||||
uid: string
|
||||
): Promise<Feature | undefined> => {
|
||||
if (name === FeatureName.UltraRealisticVoice) {
|
||||
return optInUltraRealisticVoice(uid)
|
||||
switch (name) {
|
||||
case FeatureName.UltraRealisticVoice:
|
||||
return optInLimitedFeature(
|
||||
FeatureName.UltraRealisticVoice,
|
||||
uid,
|
||||
MAX_ULTRA_REALISTIC_USERS
|
||||
)
|
||||
case FeatureName.YouTubeTranscripts:
|
||||
return optInLimitedFeature(
|
||||
FeatureName.YouTubeTranscripts,
|
||||
uid,
|
||||
MAX_YOUTUBE_TRANSCRIPT_USERS
|
||||
)
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
const optInUltraRealisticVoice = async (uid: string): Promise<Feature> => {
|
||||
const optInLimitedFeature = async (
|
||||
featureName: string,
|
||||
uid: string,
|
||||
maxUsers: number
|
||||
): Promise<Feature> => {
|
||||
const feature = await getRepository(Feature).findOne({
|
||||
where: {
|
||||
name: FeatureName.UltraRealisticVoice,
|
||||
name: featureName,
|
||||
grantedAt: Not(IsNull()),
|
||||
user: { id: uid },
|
||||
},
|
||||
|
|
@ -40,8 +59,6 @@ const optInUltraRealisticVoice = async (uid: string): Promise<Feature> => {
|
|||
return feature
|
||||
}
|
||||
|
||||
const MAX_USERS = 1500
|
||||
// opt in to feature for the first 1500 users
|
||||
const optedInFeatures: Feature[] = (await appDataSource.query(
|
||||
`insert into omnivore.features (user_id, name, granted_at)
|
||||
select $1, $2, $3 from omnivore.features
|
||||
|
|
@ -50,7 +67,7 @@ const optInUltraRealisticVoice = async (uid: string): Promise<Feature> => {
|
|||
on conflict (user_id, name)
|
||||
do update set granted_at = $3
|
||||
returning *, granted_at as "grantedAt", created_at as "createdAt", updated_at as "updatedAt";`,
|
||||
[uid, FeatureName.UltraRealisticVoice, new Date(), MAX_USERS]
|
||||
[uid, featureName, new Date(), maxUsers]
|
||||
)) as Feature[]
|
||||
|
||||
// if no new features were created then user has exceeded max users
|
||||
|
|
@ -60,7 +77,7 @@ const optInUltraRealisticVoice = async (uid: string): Promise<Feature> => {
|
|||
// create/update an opt-in record with null grantedAt
|
||||
const optInRecord = {
|
||||
user: { id: uid },
|
||||
name: FeatureName.UltraRealisticVoice,
|
||||
name: featureName,
|
||||
grantedAt: null,
|
||||
}
|
||||
const result = await getRepository(Feature).upsert(optInRecord, [
|
||||
|
|
|
|||
|
|
@ -2,20 +2,25 @@ import { DeepPartial, FindOptionsWhere } from 'typeorm'
|
|||
import { Integration } from '../../entity/integration'
|
||||
import { authTrx } from '../../repository'
|
||||
import { IntegrationClient } from './integration'
|
||||
import { NotionClient } from './notion'
|
||||
import { PocketClient } from './pocket'
|
||||
import { ReadwiseClient } from './readwise'
|
||||
|
||||
const integrations: IntegrationClient[] = [
|
||||
new ReadwiseClient(),
|
||||
new PocketClient(),
|
||||
]
|
||||
|
||||
export const getIntegrationClient = (name: string): IntegrationClient => {
|
||||
const service = integrations.find((s) => s.name === name)
|
||||
if (!service) {
|
||||
throw new Error(`Integration client not found: ${name}`)
|
||||
export const getIntegrationClient = (
|
||||
name: string,
|
||||
token: string,
|
||||
integrationData?: Integration
|
||||
): IntegrationClient => {
|
||||
switch (name.toLowerCase()) {
|
||||
case 'readwise':
|
||||
return new ReadwiseClient(token)
|
||||
case 'pocket':
|
||||
return new PocketClient(token)
|
||||
case 'notion':
|
||||
return new NotionClient(token, integrationData)
|
||||
default:
|
||||
throw new Error(`Integration client not found: ${name}`)
|
||||
}
|
||||
return service
|
||||
}
|
||||
|
||||
export const deleteIntegrations = async (
|
||||
|
|
@ -75,7 +80,11 @@ export const saveIntegration = async (
|
|||
userId: string
|
||||
) => {
|
||||
return authTrx(
|
||||
async (t) => t.getRepository(Integration).save(integration),
|
||||
async (t) => {
|
||||
const repo = t.getRepository(Integration)
|
||||
const newIntegration = await repo.save(integration)
|
||||
return repo.findOneByOrFail({ id: newIntegration.id })
|
||||
},
|
||||
undefined,
|
||||
userId
|
||||
)
|
||||
|
|
|
|||
|
|
@ -20,9 +20,11 @@ export interface RetrieveRequest {
|
|||
|
||||
export interface IntegrationClient {
|
||||
name: string
|
||||
apiUrl: string
|
||||
token: string
|
||||
|
||||
accessToken(token: string): Promise<string | null>
|
||||
accessToken(): Promise<string | null>
|
||||
|
||||
export(token: string, items: LibraryItem[]): Promise<boolean>
|
||||
auth(state: string): Promise<string>
|
||||
|
||||
export(items: LibraryItem[]): Promise<boolean>
|
||||
}
|
||||
|
|
|
|||
417
packages/api/src/services/integrations/notion.ts
Normal file
417
packages/api/src/services/integrations/notion.ts
Normal file
|
|
@ -0,0 +1,417 @@
|
|||
import { Client } from '@notionhq/client'
|
||||
import axios from 'axios'
|
||||
import { updateIntegration } from '.'
|
||||
import { Integration } from '../../entity/integration'
|
||||
import { LibraryItem } from '../../entity/library_item'
|
||||
import { env } from '../../env'
|
||||
import { Merge } from '../../util'
|
||||
import { highlightUrl } from '../../utils/helpers'
|
||||
import { logger } from '../../utils/logger'
|
||||
import { IntegrationClient } from './integration'
|
||||
|
||||
type AnnotationColor =
|
||||
| 'default'
|
||||
| 'gray'
|
||||
| 'brown'
|
||||
| 'orange'
|
||||
| 'yellow'
|
||||
| 'green'
|
||||
| 'blue'
|
||||
| 'purple'
|
||||
| 'pink'
|
||||
| 'red'
|
||||
| 'gray_background'
|
||||
| 'brown_background'
|
||||
| 'orange_background'
|
||||
| 'yellow_background'
|
||||
| 'green_background'
|
||||
| 'blue_background'
|
||||
| 'purple_background'
|
||||
| 'pink_background'
|
||||
| 'red_background'
|
||||
|
||||
interface NotionPage {
|
||||
parent: {
|
||||
database_id: string
|
||||
}
|
||||
cover?: {
|
||||
external: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
icon?: {
|
||||
external: {
|
||||
url: string
|
||||
}
|
||||
}
|
||||
properties: {
|
||||
Title: {
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: string
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Author: {
|
||||
rich_text: Array<{
|
||||
text: {
|
||||
content: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
'Original URL': {
|
||||
url: string
|
||||
}
|
||||
'Omnivore URL': {
|
||||
url: string
|
||||
}
|
||||
'Saved At': {
|
||||
date: {
|
||||
start: string
|
||||
}
|
||||
}
|
||||
'Last Updated': {
|
||||
date: {
|
||||
start: string
|
||||
}
|
||||
}
|
||||
Tags?: {
|
||||
multi_select: Array<{ name: string }>
|
||||
}
|
||||
}
|
||||
children?: Array<{
|
||||
paragraph: {
|
||||
rich_text: Array<{
|
||||
text: {
|
||||
content: string
|
||||
link?: { url: string }
|
||||
}
|
||||
annotations: {
|
||||
code: boolean
|
||||
color: AnnotationColor
|
||||
}
|
||||
}>
|
||||
children?: Array<{
|
||||
paragraph: {
|
||||
rich_text: Array<{
|
||||
text: {
|
||||
content: string
|
||||
}
|
||||
}>
|
||||
}
|
||||
}>
|
||||
}
|
||||
}>
|
||||
}
|
||||
|
||||
type Property = 'highlights'
|
||||
|
||||
interface Settings {
|
||||
parentPageId: string
|
||||
parentDatabaseId: string
|
||||
properties: Property[]
|
||||
}
|
||||
|
||||
export class NotionClient implements IntegrationClient {
|
||||
name = 'NOTION'
|
||||
token: string
|
||||
|
||||
private headers = {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
'Notion-Version': '2022-06-28',
|
||||
}
|
||||
private timeout = 5000 // 5 seconds
|
||||
private axiosInstance = axios.create({
|
||||
baseURL: 'https://api.notion.com/v1',
|
||||
timeout: this.timeout,
|
||||
})
|
||||
|
||||
private client: Client
|
||||
private integrationData?: Merge<Integration, { settings?: Settings }>
|
||||
|
||||
constructor(token: string, integration?: Integration) {
|
||||
this.token = token
|
||||
this.client = new Client({
|
||||
auth: token,
|
||||
timeoutMs: this.timeout,
|
||||
})
|
||||
this.integrationData = integration
|
||||
}
|
||||
|
||||
accessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
// encode in base 64
|
||||
const encoded = Buffer.from(
|
||||
`${env.notion.clientId}:${env.notion.clientSecret}`
|
||||
).toString('base64')
|
||||
|
||||
const response = await this.axiosInstance.post<{ access_token: string }>(
|
||||
'/oauth/token',
|
||||
{
|
||||
grant_type: 'authorization_code',
|
||||
code: this.token,
|
||||
redirect_uri: `${env.client.url}/settings/integrations`,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
...this.headers,
|
||||
Authorization: `Basic ${encoded}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
return response.data.access_token
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
} else {
|
||||
logger.error(error)
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async auth(): Promise<string> {
|
||||
return Promise.resolve(env.notion.authUrl)
|
||||
}
|
||||
|
||||
private itemToNotionPage = (
|
||||
item: LibraryItem,
|
||||
settings: Settings,
|
||||
lastSync?: Date | null
|
||||
): NotionPage => {
|
||||
return {
|
||||
parent: {
|
||||
database_id: settings.parentDatabaseId,
|
||||
},
|
||||
icon: item.siteIcon
|
||||
? {
|
||||
external: {
|
||||
url: item.siteIcon,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
cover: item.thumbnail
|
||||
? {
|
||||
external: {
|
||||
url: item.thumbnail,
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
properties: {
|
||||
Title: {
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: item.title,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
Author: {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: item.author || 'unknown',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
'Original URL': {
|
||||
url: item.originalUrl,
|
||||
},
|
||||
'Omnivore URL': {
|
||||
url: `${env.client.url}/me/${item.slug}`,
|
||||
},
|
||||
'Saved At': {
|
||||
date: {
|
||||
start: item.createdAt.toISOString(),
|
||||
},
|
||||
},
|
||||
'Last Updated': {
|
||||
date: {
|
||||
start: item.updatedAt.toISOString(),
|
||||
},
|
||||
},
|
||||
Tags: item.labels
|
||||
? {
|
||||
multi_select: item.labels.map((label) => ({
|
||||
name: label.name,
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
children:
|
||||
settings.properties.includes('highlights') && item.highlights
|
||||
? item.highlights
|
||||
.filter(
|
||||
(highlight) => !lastSync || highlight.updatedAt > lastSync // only new highlights
|
||||
)
|
||||
.map((highlight) => ({
|
||||
paragraph: {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: highlight.quote || '',
|
||||
link: {
|
||||
url: highlightUrl(item.slug, highlight.id),
|
||||
},
|
||||
},
|
||||
annotations: {
|
||||
code: true,
|
||||
color: highlight.color as AnnotationColor,
|
||||
},
|
||||
},
|
||||
],
|
||||
children: highlight.annotation
|
||||
? [
|
||||
{
|
||||
paragraph: {
|
||||
rich_text: [
|
||||
{
|
||||
text: {
|
||||
content: highlight.annotation || '',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
},
|
||||
}))
|
||||
: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
private createPage = async (page: NotionPage) => {
|
||||
await this.client.pages.create(page)
|
||||
}
|
||||
|
||||
private findPage = async (url: string, databaseId: string) => {
|
||||
const response = await this.client.databases.query({
|
||||
database_id: databaseId,
|
||||
page_size: 1,
|
||||
filter: {
|
||||
property: 'Omnivore URL',
|
||||
url: {
|
||||
equals: url,
|
||||
},
|
||||
},
|
||||
})
|
||||
if (response.results.length > 0) {
|
||||
return response.results[0]
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export = async (items: LibraryItem[]): Promise<boolean> => {
|
||||
const settings = this.integrationData?.settings
|
||||
if (!this.integrationData || !settings) {
|
||||
logger.error('Notion integration data not found')
|
||||
return false
|
||||
}
|
||||
|
||||
const pageId = settings.parentPageId
|
||||
if (!pageId) {
|
||||
logger.error('Notion parent page id not found')
|
||||
return false
|
||||
}
|
||||
|
||||
let databaseId = settings.parentDatabaseId
|
||||
if (!databaseId) {
|
||||
// create a database for the items
|
||||
const database = await this.client.databases.create({
|
||||
parent: {
|
||||
page_id: pageId,
|
||||
},
|
||||
title: [
|
||||
{
|
||||
text: {
|
||||
content: 'Library',
|
||||
},
|
||||
},
|
||||
],
|
||||
description: [
|
||||
{
|
||||
text: {
|
||||
content: 'Library of saved items from Omnivore',
|
||||
},
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
Title: {
|
||||
title: {},
|
||||
},
|
||||
Author: {
|
||||
rich_text: {},
|
||||
},
|
||||
'Original URL': {
|
||||
url: {},
|
||||
},
|
||||
'Omnivore URL': {
|
||||
url: {},
|
||||
},
|
||||
'Saved At': {
|
||||
date: {},
|
||||
},
|
||||
'Last Updated': {
|
||||
date: {},
|
||||
},
|
||||
Tags: {
|
||||
multi_select: {},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// save the database id
|
||||
databaseId = database.id
|
||||
settings.parentDatabaseId = databaseId
|
||||
await updateIntegration(
|
||||
this.integrationData.id,
|
||||
{
|
||||
settings,
|
||||
},
|
||||
this.integrationData.user.id
|
||||
)
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
items.map(async (item) => {
|
||||
const notionPage = this.itemToNotionPage(
|
||||
item,
|
||||
settings,
|
||||
this.integrationData?.syncedAt
|
||||
)
|
||||
const url = notionPage.properties['Omnivore URL'].url
|
||||
|
||||
const existingPage = await this.findPage(url, databaseId)
|
||||
if (existingPage) {
|
||||
// update the page
|
||||
await this.client.pages.update({
|
||||
page_id: existingPage.id,
|
||||
properties: notionPage.properties,
|
||||
})
|
||||
|
||||
// append the children incrementally
|
||||
if (notionPage.children && notionPage.children.length > 0) {
|
||||
await this.client.blocks.children.append({
|
||||
block_id: existingPage.id,
|
||||
children: notionPage.children,
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// create the page
|
||||
return this.createPage(notionPage)
|
||||
})
|
||||
)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -5,24 +5,27 @@ import { IntegrationClient } from './integration'
|
|||
|
||||
export class PocketClient implements IntegrationClient {
|
||||
name = 'POCKET'
|
||||
apiUrl = 'https://getpocket.com/v3'
|
||||
headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Accept': 'application/json',
|
||||
token: string
|
||||
_axios = axios.create({
|
||||
baseURL: 'https://getpocket.com/v3',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-Accept': 'application/json',
|
||||
},
|
||||
timeout: 5000, // 5 seconds
|
||||
})
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token
|
||||
}
|
||||
|
||||
accessToken = async (token: string): Promise<string | null> => {
|
||||
const url = `${this.apiUrl}/oauth/authorize`
|
||||
accessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
const response = await axios.post<{ access_token: string }>(
|
||||
url,
|
||||
const response = await this._axios.post<{ access_token: string }>(
|
||||
'/oauth/authorize',
|
||||
{
|
||||
consumer_key: env.pocket.consumerKey,
|
||||
code: token,
|
||||
},
|
||||
{
|
||||
headers: this.headers,
|
||||
timeout: 5000, // 5 seconds
|
||||
code: this.token,
|
||||
}
|
||||
)
|
||||
return response.data.access_token
|
||||
|
|
@ -36,7 +39,26 @@ export class PocketClient implements IntegrationClient {
|
|||
}
|
||||
}
|
||||
|
||||
export = async (): Promise<boolean> => {
|
||||
return Promise.resolve(false)
|
||||
export = () => {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
async auth(state: string) {
|
||||
const consumerKey = env.pocket.consumerKey
|
||||
const redirectUri = `${env.client.url}/settings/integrations`
|
||||
|
||||
// make a POST request to Pocket to get a request token
|
||||
const response = await this._axios.post<{ code: string }>(
|
||||
'/oauth/request',
|
||||
{
|
||||
consumer_key: consumerKey,
|
||||
redirect_uri: redirectUri,
|
||||
}
|
||||
)
|
||||
const { code } = response.data
|
||||
|
||||
return `https://getpocket.com/auth/authorize?request_token=${code}&redirect_uri=${redirectUri}${encodeURIComponent(
|
||||
`?pocketToken=${code}&state=${state}`
|
||||
)}`
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,17 +33,29 @@ interface ReadwiseHighlight {
|
|||
|
||||
export class ReadwiseClient implements IntegrationClient {
|
||||
name = 'READWISE'
|
||||
apiUrl = 'https://readwise.io/api/v2'
|
||||
token: string
|
||||
|
||||
accessToken = async (token: string): Promise<string | null> => {
|
||||
const authUrl = `${this.apiUrl}/auth`
|
||||
_headers = {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
_axios = axios.create({
|
||||
baseURL: 'https://readwise.io/api/v2',
|
||||
timeout: 5000, // 5 seconds
|
||||
})
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token
|
||||
}
|
||||
|
||||
accessToken = async (): Promise<string | null> => {
|
||||
try {
|
||||
const response = await axios.get(authUrl, {
|
||||
const response = await this._axios.get('/auth', {
|
||||
headers: {
|
||||
Authorization: `Token ${token}`,
|
||||
...this._headers,
|
||||
Authorization: `Token ${this.token}`,
|
||||
},
|
||||
})
|
||||
return response.status === 204 ? token : null
|
||||
return response.status === 204 ? this.token : null
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error)) {
|
||||
logger.error(error.response)
|
||||
|
|
@ -54,20 +66,26 @@ export class ReadwiseClient implements IntegrationClient {
|
|||
}
|
||||
}
|
||||
|
||||
export = async (token: string, items: LibraryItem[]): Promise<boolean> => {
|
||||
export = async (items: LibraryItem[]): Promise<boolean> => {
|
||||
let result = true
|
||||
|
||||
const highlights = items.flatMap(this.itemToReadwiseHighlight)
|
||||
const highlights = items.flatMap(this._itemToReadwiseHighlight)
|
||||
|
||||
// If there are no highlights, we will skip the sync
|
||||
if (highlights.length > 0) {
|
||||
result = await this.syncWithReadwise(token, highlights)
|
||||
result = await this._syncWithReadwise(highlights)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
itemToReadwiseHighlight = (item: LibraryItem): ReadwiseHighlight[] => {
|
||||
auth = () => {
|
||||
throw new Error('Method not implemented.')
|
||||
}
|
||||
|
||||
private _itemToReadwiseHighlight = (
|
||||
item: LibraryItem
|
||||
): ReadwiseHighlight[] => {
|
||||
const category = item.siteName === 'Twitter' ? 'tweets' : 'articles'
|
||||
return item.highlights
|
||||
?.map((highlight) => {
|
||||
|
|
@ -93,22 +111,19 @@ export class ReadwiseClient implements IntegrationClient {
|
|||
.filter((highlight) => highlight !== undefined) as ReadwiseHighlight[]
|
||||
}
|
||||
|
||||
syncWithReadwise = async (
|
||||
token: string,
|
||||
private _syncWithReadwise = async (
|
||||
highlights: ReadwiseHighlight[]
|
||||
): Promise<boolean> => {
|
||||
const url = `${this.apiUrl}/highlights`
|
||||
const response = await axios.post(
|
||||
url,
|
||||
const response = await this._axios.post(
|
||||
'/highlights',
|
||||
{
|
||||
highlights,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Token ${token}`,
|
||||
'Content-Type': 'application/json',
|
||||
...this._headers,
|
||||
Authorization: `Token ${this.token}`,
|
||||
},
|
||||
timeout: 5000, // 5 seconds
|
||||
}
|
||||
)
|
||||
return response.status === 200
|
||||
|
|
|
|||
|
|
@ -700,10 +700,16 @@ export const findRecentLibraryItems = async (
|
|||
}
|
||||
|
||||
export const findLibraryItemsByIds = async (ids: string[], userId: string) => {
|
||||
const selectColumns = getColumns(libraryItemRepository)
|
||||
.filter(
|
||||
(column) => column !== 'readableContent' && column !== 'originalContent'
|
||||
)
|
||||
.map((column) => `library_item.${column}`)
|
||||
return authTrx(
|
||||
async (tx) =>
|
||||
tx
|
||||
.createQueryBuilder(LibraryItem, 'library_item')
|
||||
.select(selectColumns)
|
||||
.leftJoinAndSelect('library_item.labels', 'labels')
|
||||
.leftJoinAndSelect('library_item.highlights', 'highlights')
|
||||
.where('library_item.id IN (:...ids)', { ids })
|
||||
|
|
@ -989,7 +995,7 @@ export const createOrUpdateLibraryItem = async (
|
|||
)
|
||||
}
|
||||
|
||||
if (skipPubSub) {
|
||||
if (skipPubSub || libraryItem.state === LibraryItemState.Processing) {
|
||||
return newLibraryItem
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -113,6 +113,11 @@ export interface BackendEnv {
|
|||
mq: redisConfig
|
||||
cache: redisConfig
|
||||
}
|
||||
notion: {
|
||||
clientId: string
|
||||
clientSecret: string
|
||||
authUrl: string
|
||||
}
|
||||
}
|
||||
|
||||
const nullableEnvVars = [
|
||||
|
|
@ -165,6 +170,9 @@ const nullableEnvVars = [
|
|||
'MQ_REDIS_CERT',
|
||||
'IMPORTER_METRICS_COLLECTOR_URL',
|
||||
'INTERNAL_API_URL',
|
||||
'NOTION_CLIENT_ID',
|
||||
'NOTION_CLIENT_SECRET',
|
||||
'NOTION_AUTH_URL',
|
||||
] // Allow some vars to be null/empty
|
||||
|
||||
/* If not in GAE and Prod/QA/Demo env (f.e. on localhost/dev env), allow following env vars to be null */
|
||||
|
|
@ -311,6 +319,11 @@ export function getEnv(): BackendEnv {
|
|||
cert: parse('REDIS_CERT')?.replace(/\\n/g, '\n'), // replace \n with new line
|
||||
},
|
||||
}
|
||||
const notion = {
|
||||
clientId: parse('NOTION_CLIENT_ID'),
|
||||
clientSecret: parse('NOTION_CLIENT_SECRET'),
|
||||
authUrl: parse('NOTION_AUTH_URL'),
|
||||
}
|
||||
|
||||
return {
|
||||
pg,
|
||||
|
|
@ -333,6 +346,7 @@ export function getEnv(): BackendEnv {
|
|||
pocket,
|
||||
subscription,
|
||||
redis,
|
||||
notion,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ import { stringToHash } from './helpers'
|
|||
import { logger } from './logger'
|
||||
import View = google.cloud.tasks.v2.Task.View
|
||||
import { AISummarizeJobData, AI_SUMMARIZE_JOB_NAME } from '../jobs/ai-summarize'
|
||||
import {
|
||||
PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME,
|
||||
PROCESS_YOUTUBE_VIDEO_JOB_NAME,
|
||||
ProcessYouTubeTranscriptJobData,
|
||||
ProcessYouTubeVideoJobData,
|
||||
} from '../jobs/process-youtube-video'
|
||||
|
||||
// Instantiates a client.
|
||||
const client = new CloudTasksClient()
|
||||
|
|
@ -67,10 +73,13 @@ export const getJobPriority = (jobName: string): number => {
|
|||
case TRIGGER_RULE_JOB_NAME:
|
||||
case CALL_WEBHOOK_JOB_NAME:
|
||||
case AI_SUMMARIZE_JOB_NAME:
|
||||
case PROCESS_YOUTUBE_VIDEO_JOB_NAME:
|
||||
return 5
|
||||
case BULK_ACTION_JOB_NAME:
|
||||
case `${REFRESH_FEED_JOB_NAME}_high`:
|
||||
return 10
|
||||
case PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME:
|
||||
return 20
|
||||
case `${REFRESH_FEED_JOB_NAME}_low`:
|
||||
case EXPORT_ITEM_JOB_NAME:
|
||||
return 50
|
||||
|
|
@ -78,6 +87,7 @@ export const getJobPriority = (jobName: string): number => {
|
|||
case REFRESH_ALL_FEEDS_JOB_NAME:
|
||||
case THUMBNAIL_JOB:
|
||||
return 100
|
||||
|
||||
default:
|
||||
logger.error(`unknown job name: ${jobName}`)
|
||||
return 1
|
||||
|
|
@ -708,6 +718,36 @@ export const enqueueAISummarizeJob = async (data: AISummarizeJobData) => {
|
|||
})
|
||||
}
|
||||
|
||||
export const enqueueProcessYouTubeVideo = async (
|
||||
data: ProcessYouTubeVideoJobData
|
||||
) => {
|
||||
const queue = await getBackendQueue()
|
||||
if (!queue) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return queue.add(PROCESS_YOUTUBE_VIDEO_JOB_NAME, data, {
|
||||
priority: getJobPriority(PROCESS_YOUTUBE_VIDEO_JOB_NAME),
|
||||
attempts: 3,
|
||||
delay: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
export const enqueueProcessYouTubeTranscript = async (
|
||||
data: ProcessYouTubeTranscriptJobData
|
||||
) => {
|
||||
const queue = await getBackendQueue()
|
||||
if (!queue) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return queue.add(PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME, data, {
|
||||
priority: getJobPriority(PROCESS_YOUTUBE_TRANSCRIPT_JOB_NAME),
|
||||
attempts: 3,
|
||||
delay: 2000,
|
||||
})
|
||||
}
|
||||
|
||||
export const bulkEnqueueUpdateLabels = async (data: UpdateLabelsData[]) => {
|
||||
const queue = await getBackendQueue()
|
||||
if (!queue) {
|
||||
|
|
|
|||
101
packages/api/test/jobs/process-youtube-job.test.ts
Normal file
101
packages/api/test/jobs/process-youtube-job.test.ts
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
import { expect } from 'chai'
|
||||
import 'mocha'
|
||||
import { addTranscriptChapters } from '../../src/jobs/process-youtube-video'
|
||||
|
||||
describe('create transcript', () => {
|
||||
describe('build items', () => {
|
||||
it('properly adds chapter headers to transcript', async () => {
|
||||
const chapters = [
|
||||
{
|
||||
title: 'Intro',
|
||||
start: 0,
|
||||
},
|
||||
{
|
||||
title: "Joe Biden's re-election effort",
|
||||
start: 22000,
|
||||
},
|
||||
{
|
||||
title: 'Ad break',
|
||||
start: 909000,
|
||||
},
|
||||
{
|
||||
title: "Trump's crazy speech & Orbán relationship",
|
||||
start: 1060000,
|
||||
},
|
||||
]
|
||||
const transcript = [
|
||||
{
|
||||
text: "welcome to pod save America I'm John",
|
||||
duration: 3280,
|
||||
start: 80,
|
||||
},
|
||||
{
|
||||
text: "favro I'm John L I'm Tommy VOR on",
|
||||
duration: 3480,
|
||||
start: 1480,
|
||||
},
|
||||
{
|
||||
text: "today's show Donald Trump kicks off the",
|
||||
duration: 3320,
|
||||
start: 3360,
|
||||
},
|
||||
{
|
||||
text: 'general election by mocking Joe Biden',
|
||||
duration: 3400,
|
||||
start: 4960,
|
||||
},
|
||||
{
|
||||
text: 'stutter hosting a concert for Victor',
|
||||
duration: 3680,
|
||||
start: 6680,
|
||||
},
|
||||
{
|
||||
text: 'Orban and floating cuts to Medicare and',
|
||||
duration: 4239,
|
||||
start: 8360,
|
||||
},
|
||||
{
|
||||
text: 'Social Security Alabama Senator Katie',
|
||||
duration: 3840,
|
||||
start: 10360,
|
||||
},
|
||||
{
|
||||
text: 'Brit and Republicans are still dealing',
|
||||
duration: 3401,
|
||||
start: 12599,
|
||||
},
|
||||
{
|
||||
text: 'with the Fallout from what may have been',
|
||||
duration: 3320,
|
||||
start: 14200,
|
||||
},
|
||||
{
|
||||
text: 'the worst ever State of the Union',
|
||||
duration: 4600,
|
||||
start: 16000,
|
||||
},
|
||||
{
|
||||
text: 'response and later take appreciator is',
|
||||
duration: 6640,
|
||||
start: 17520,
|
||||
},
|
||||
{
|
||||
text: 'back so is Elijah uh but first the man',
|
||||
duration: 6519,
|
||||
start: 20600,
|
||||
},
|
||||
{
|
||||
text: 'Sean Hannity now calls jacked up Joe has',
|
||||
duration: 4680,
|
||||
start: 24160,
|
||||
},
|
||||
]
|
||||
|
||||
const res = addTranscriptChapters(chapters, transcript)
|
||||
console.log('res: ', res)
|
||||
|
||||
expect(res.length).to.eq(17)
|
||||
expect(res[13].text).to.eq("\n\n## Joe Biden's re-election effort\n\n")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -640,6 +640,7 @@ describe('Article API', () => {
|
|||
context('when the source is rss-feeder and url is from youtube.com', () => {
|
||||
const source = 'rss-feeder'
|
||||
const stub = sinon.stub(createTask, 'enqueueParseRequest')
|
||||
const stub2 = sinon.stub(createTask, 'enqueueProcessYouTubeVideo')
|
||||
|
||||
before(() => {
|
||||
url = 'https://www.youtube.com/watch?v=123'
|
||||
|
|
|
|||
|
|
@ -222,17 +222,6 @@ describe('Integrations resolvers', () => {
|
|||
expect(res.body.data.setIntegration.integration.enabled).to.be
|
||||
.false
|
||||
})
|
||||
|
||||
it('deletes cloud task', async () => {
|
||||
const res = await graphqlRequest(
|
||||
query(integrationId, integrationName, token, enabled),
|
||||
authToken
|
||||
)
|
||||
const integration = await findIntegration({
|
||||
id: res.body.data.setIntegration.integration.id,
|
||||
}, loginUser.id)
|
||||
expect(integration?.taskName).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
context('when enable is true', () => {
|
||||
|
|
|
|||
|
|
@ -591,7 +591,7 @@ describe('auth router', () => {
|
|||
await deleteUser(user.id)
|
||||
})
|
||||
|
||||
it('adds popular reads to the library', async () => {
|
||||
it('adds popular reads to the continue reading section', async () => {
|
||||
const pendingUserToken = await createPendingUserToken({
|
||||
sourceUserId,
|
||||
email,
|
||||
|
|
@ -608,7 +608,7 @@ describe('auth router', () => {
|
|||
).expect(200)
|
||||
const user = await userRepository.findOneByOrFail({ name })
|
||||
const { count } = await searchLibraryItems(
|
||||
{ query: 'in:all' },
|
||||
{ query: 'in:inbox sort:read-desc is:reading' },
|
||||
user.id
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -86,9 +86,14 @@ export class YoutubeHandler extends ContentHandler {
|
|||
<meta property="og:type" content="video" />
|
||||
</head>
|
||||
<body>
|
||||
<iframe width="${width}" height="${height}" src="${src}" title="${escapedTitle}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<p><a href="${url}" target="_blank">${escapedTitle}</a></p>
|
||||
<p itemscope="" itemprop="author" itemtype="http://schema.org/Person">By <a href="${oembed.author_url}" target="_blank">${authorName}</a></p>
|
||||
<div>
|
||||
<article id="_omnivore_youtube">
|
||||
<iframe id="_omnivore_youtube_video" width="${width}" height="${height}" src="${src}" title="${escapedTitle}" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
|
||||
<p><a href="${url}" target="_blank">${escapedTitle}</a></p>
|
||||
<p itemscope="" itemprop="author" itemtype="http://schema.org/Person">By <a href="${oembed.author_url}" target="_blank">${authorName}</a></p>
|
||||
<div id="_omnivore_youtube_transcript"></div>
|
||||
</article>
|
||||
</div>
|
||||
</body>
|
||||
</html>`
|
||||
|
||||
|
|
|
|||
9
packages/db/migrations/0167.do.add_settings_column_to_integrations.sql
Executable file
9
packages/db/migrations/0167.do.add_settings_column_to_integrations.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: DO
|
||||
-- Name: add_settings_column_to_integrations
|
||||
-- Description: Add settings column to integrations table
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.integrations ADD COLUMN settings jsonb;
|
||||
|
||||
COMMIT;
|
||||
9
packages/db/migrations/0167.undo.add_settings_column_to_integrations.sql
Executable file
9
packages/db/migrations/0167.undo.add_settings_column_to_integrations.sql
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
-- Type: UNDO
|
||||
-- Name: add_settings_column_to_integrations
|
||||
-- Description: Add settings column to integrations table
|
||||
|
||||
BEGIN;
|
||||
|
||||
ALTER TABLE omnivore.integrations DROP COLUMN settings;
|
||||
|
||||
COMMIT;
|
||||
|
|
@ -81,14 +81,14 @@ export function CardMenu(props: CardMenuProps): JSX.Element {
|
|||
}}
|
||||
title="Remove"
|
||||
/>
|
||||
{!!props.item.subscription && (
|
||||
{/* {!!props.item.subscription && (
|
||||
<DropdownOption
|
||||
onSelect={() => {
|
||||
props.actionHandler('unsubscribe')
|
||||
}}
|
||||
title="Unsubscribe"
|
||||
/>
|
||||
)}
|
||||
)} */}
|
||||
</Dropdown>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,10 +21,11 @@ type ConfirmationModalProps = {
|
|||
export function ConfirmationModal(props: ConfirmationModalProps): JSX.Element {
|
||||
const safeOnOpenChange = useCallback(
|
||||
(open: boolean) => {
|
||||
props.onOpenChange(open)
|
||||
setTimeout(() => {
|
||||
console.log('body style: ', document.body.style)
|
||||
document.body.style.removeProperty('pointer-events')
|
||||
}, 200)
|
||||
props.onOpenChange(open)
|
||||
},
|
||||
[props]
|
||||
)
|
||||
|
|
|
|||
5
packages/web/components/templates/Beta.tsx
Normal file
5
packages/web/components/templates/Beta.tsx
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { Alert } from 'antd'
|
||||
|
||||
export function Beta(): JSX.Element {
|
||||
return <Alert message="Beta" type="warning" showIcon />
|
||||
}
|
||||
47
packages/web/components/templates/EmptyLayout.tsx
Normal file
47
packages/web/components/templates/EmptyLayout.tsx
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { PageMetaData } from '../patterns/PageMetaData'
|
||||
import { DEFAULT_HEADER_HEIGHT } from './homeFeed/HeaderSpacer'
|
||||
import { SettingsDropdown } from './navMenu/SettingsDropdown'
|
||||
|
||||
type EmptyLayoutProps = {
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function EmptyLayout(props: EmptyLayoutProps): JSX.Element {
|
||||
return (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ width: '100%', height: '100%', minHeight: '100vh' }}
|
||||
>
|
||||
<PageMetaData path="/" title={props.title} />
|
||||
<VStack css={{ width: '100%', height: '100%' }}>
|
||||
<Box
|
||||
css={{
|
||||
height: DEFAULT_HEADER_HEIGHT,
|
||||
'@mdDown': {
|
||||
display: 'none',
|
||||
},
|
||||
}}
|
||||
></Box>
|
||||
<Box
|
||||
css={{
|
||||
p: '15px',
|
||||
display: 'none',
|
||||
height: DEFAULT_HEADER_HEIGHT,
|
||||
'@mdDown': {
|
||||
display: 'flex',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SettingsDropdown />
|
||||
</Box>
|
||||
<HStack css={{ width: '100%', height: '100%' }} distribution="start">
|
||||
{props.children}
|
||||
</HStack>
|
||||
<Box css={{ height: '120px', width: '100%' }} />
|
||||
</VStack>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
import { Box, HStack, VStack } from '../elements/LayoutPrimitives'
|
||||
import { useGetViewerQuery } from '../../lib/networking/queries/useGetViewerQuery'
|
||||
import { navigationCommands } from '../../lib/keyboardShortcuts/navigationShortcuts'
|
||||
import { useKeyboardShortcuts } from '../../lib/keyboardShortcuts/useKeyboardShortcuts'
|
||||
import { useRouter } from 'next/router'
|
||||
|
|
|
|||
|
|
@ -115,6 +115,28 @@ export function Article(props: ArticleProps): JSX.Element {
|
|||
}
|
||||
}, 2500)
|
||||
|
||||
useEffect(() => {
|
||||
const youtubePlayer = document.getElementById('_omnivore_youtube_video')
|
||||
|
||||
const updateScroll = () => {
|
||||
console.log('scroll y: ', window.scrollY, youtubePlayer)
|
||||
|
||||
if (youtubePlayer) {
|
||||
if (window.scrollY > 200) {
|
||||
youtubePlayer.classList.add('is-sticky')
|
||||
} else {
|
||||
youtubePlayer.classList.remove('is-sticky')
|
||||
}
|
||||
}
|
||||
}
|
||||
if (youtubePlayer) {
|
||||
window.addEventListener('scroll', updateScroll)
|
||||
}
|
||||
return () => {
|
||||
window.removeEventListener('scroll', updateScroll) // clean up
|
||||
}
|
||||
}, [props])
|
||||
|
||||
// Scroll to initial anchor position
|
||||
useEffect(() => {
|
||||
if (typeof window === 'undefined') {
|
||||
|
|
|
|||
|
|
@ -1,27 +1,18 @@
|
|||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { styled } from '@stitches/react'
|
||||
import Image from 'next/image'
|
||||
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { Button } from '../../elements/Button'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
|
||||
import { useRouter } from 'next/router'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { deleteIntegrationMutation } from '../../../lib/networking/mutations/deleteIntegrationMutation'
|
||||
import { setIntegrationMutation } from '../../../lib/networking/mutations/setIntegrationMutation'
|
||||
import {
|
||||
Integration,
|
||||
useGetIntegrationsQuery,
|
||||
} from '../../../lib/networking/queries/useGetIntegrationsQuery'
|
||||
import { useRouter } from 'next/router'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { deleteIntegrationMutation } from '../../../lib/networking/mutations/deleteIntegrationMutation'
|
||||
|
||||
// Styles
|
||||
const Header = styled(Box, {
|
||||
color: '$utilityTextDefault',
|
||||
fontSize: 'x-large',
|
||||
margin: '20px',
|
||||
})
|
||||
import { Button } from '../../elements/Button'
|
||||
import { FormInput } from '../../elements/FormElements'
|
||||
import { HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { Header } from '../settings/SettingsTable'
|
||||
|
||||
export function Readwise(): JSX.Element {
|
||||
const { integrations, revalidate } = useGetIntegrationsQuery()
|
||||
|
|
|
|||
|
|
@ -6,11 +6,18 @@ import { MoreOptionsIcon } from '../../elements/images/MoreOptionsIcon'
|
|||
import { InfoLink } from '../../elements/InfoLink'
|
||||
import { Box, HStack, SpanBox, VStack } from '../../elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../elements/StyledText'
|
||||
import { theme } from '../../tokens/stitches.config'
|
||||
import { styled, theme } from '../../tokens/stitches.config'
|
||||
import { SettingsLayout } from '../SettingsLayout'
|
||||
import { usePersistedState } from '../../../lib/hooks/usePersistedState'
|
||||
import { FeatureHelpBox } from '../../elements/FeatureHelpBox'
|
||||
|
||||
// Styles
|
||||
export const Header = styled(Box, {
|
||||
color: '$utilityTextDefault',
|
||||
fontSize: 'x-large',
|
||||
margin: '20px',
|
||||
})
|
||||
|
||||
type SettingsTableProps = {
|
||||
pageId: string
|
||||
pageInfoLink?: string | undefined
|
||||
|
|
|
|||
|
|
@ -0,0 +1,49 @@
|
|||
import { gql } from 'graphql-request'
|
||||
import { gqlFetcher } from '../networkHelpers'
|
||||
|
||||
export interface OptInFeatureInput {
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface OptInFeatureSuccess {
|
||||
feature: { id: string }
|
||||
}
|
||||
|
||||
interface Response {
|
||||
optInFeature: OptInFeatureSuccess
|
||||
}
|
||||
|
||||
export async function optInFeature(
|
||||
input: OptInFeatureInput
|
||||
): Promise<boolean | undefined> {
|
||||
const mutation = gql`
|
||||
mutation OptInFeature($input: OptInFeatureInput!) {
|
||||
optInFeature(input: $input) {
|
||||
... on OptInFeatureSuccess {
|
||||
feature {
|
||||
id
|
||||
}
|
||||
}
|
||||
... on OptInFeatureError {
|
||||
errorCodes
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
try {
|
||||
const data = await gqlFetcher(mutation, {
|
||||
input,
|
||||
})
|
||||
const output = data as Response | undefined
|
||||
if (
|
||||
!output ||
|
||||
!output.optInFeature ||
|
||||
'errorCodes' in output?.optInFeature
|
||||
) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} catch (err) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ export type SetIntegrationInput = {
|
|||
token: string
|
||||
enabled: boolean
|
||||
importItemState?: ImportItemState
|
||||
settings?: any
|
||||
}
|
||||
|
||||
type SetIntegrationResult = {
|
||||
|
|
@ -52,6 +53,7 @@ export async function setIntegrationMutation(
|
|||
enabled
|
||||
createdAt
|
||||
updatedAt
|
||||
settings
|
||||
}
|
||||
}
|
||||
... on SetIntegrationError {
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ type Unsubscribe = {
|
|||
}
|
||||
|
||||
export async function unsubscribeMutation(
|
||||
subscribeName: string,
|
||||
id = ''
|
||||
subscribtionName: string,
|
||||
id: string
|
||||
): Promise<any | undefined> {
|
||||
const mutation = gql`
|
||||
mutation {
|
||||
unsubscribe(name: "${subscribeName}", subscriptionId: "${id}") {
|
||||
mutation Unsubscribe($subscribtionName: String!, $subscriptionId: ID!) {
|
||||
unsubscribe(name: $subscribtionName, subscriptionId: $subscriptionId) {
|
||||
... on UnsubscribeSuccess {
|
||||
subscription {
|
||||
id
|
||||
|
|
@ -31,7 +31,11 @@ export async function unsubscribeMutation(
|
|||
`
|
||||
|
||||
try {
|
||||
const data = (await gqlFetcher(mutation)) as UnsubscribeResult
|
||||
const data = (await gqlFetcher(mutation, {
|
||||
subscriptionId: id,
|
||||
subscribtionName: subscribtionName,
|
||||
})) as UnsubscribeResult
|
||||
|
||||
return data.unsubscribe.errorCodes
|
||||
? undefined
|
||||
: data.unsubscribe.subscription.id
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ export interface Integration {
|
|||
createdAt: Date
|
||||
updatedAt: Date
|
||||
taskName?: string
|
||||
settings?: any
|
||||
}
|
||||
|
||||
export type IntegrationType = 'EXPORT' | 'IMPORT'
|
||||
|
|
@ -43,6 +44,7 @@ export function useGetIntegrationsQuery(): IntegrationsQueryResponse {
|
|||
createdAt
|
||||
updatedAt
|
||||
taskName
|
||||
settings
|
||||
}
|
||||
}
|
||||
... on IntegrationsError {
|
||||
|
|
|
|||
|
|
@ -413,27 +413,27 @@ export function useGetLibraryItemsQuery({
|
|||
readingProgressAnchorIndex: 0,
|
||||
})
|
||||
break
|
||||
case 'unsubscribe':
|
||||
if (!!item.node.subscription) {
|
||||
updateData({
|
||||
cursor: item.cursor,
|
||||
node: {
|
||||
...item.node,
|
||||
subscription: undefined,
|
||||
},
|
||||
})
|
||||
unsubscribeMutation(item.node.subscription).then((res) => {
|
||||
if (res) {
|
||||
showSuccessToast('Unsubscribed successfully', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
} else {
|
||||
showErrorToast('Error unsubscribing', {
|
||||
position: 'bottom-right',
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
// case 'unsubscribe':
|
||||
// if (!!item.node.subscription) {
|
||||
// updateData({
|
||||
// cursor: item.cursor,
|
||||
// node: {
|
||||
// ...item.node,
|
||||
// subscription: undefined,
|
||||
// },
|
||||
// })
|
||||
// unsubscribeMutation(item.node.subscription).then((res) => {
|
||||
// if (res) {
|
||||
// showSuccessToast('Unsubscribed successfully', {
|
||||
// position: 'bottom-right',
|
||||
// })
|
||||
// } else {
|
||||
// showErrorToast('Error unsubscribing', {
|
||||
// position: 'bottom-right',
|
||||
// })
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
case 'update-item':
|
||||
updateData(item)
|
||||
break
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import useSWR from 'swr'
|
|||
import { publicGqlFetcher } from '../networkHelpers'
|
||||
|
||||
type ViewerQueryResponse = {
|
||||
mutate: () => void
|
||||
viewerData?: ViewerQueryResponseData
|
||||
viewerDataError?: unknown
|
||||
isLoading: boolean
|
||||
|
|
@ -51,9 +52,10 @@ export function useGetViewerQuery(): ViewerQueryResponse {
|
|||
}
|
||||
`
|
||||
|
||||
const { data, error } = useSWR(query, publicGqlFetcher)
|
||||
const { data, error, mutate } = useSWR(query, publicGqlFetcher)
|
||||
|
||||
return {
|
||||
mutate,
|
||||
viewerData: data as ViewerQueryResponseData,
|
||||
viewerDataError: error, // TODO: figure out error possibilities
|
||||
isLoading: !error && !data,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ const ContentSecurityPolicy = `
|
|||
base-uri 'self';
|
||||
connect-src 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://proxy-prod.omnivore-image-cache.app https://accounts.google.com https://proxy-demo.omnivore-image-cache.app https://storage.googleapis.com https://api.segment.io https://cdn.segment.com https://widget.intercom.io https://api-iam.intercom.io https://static.intercomassets.com https://downloads.intercomcdn.com https://platform.twitter.com wss://nexus-websocket-a.intercom.io wss://nexus-websocket-b.intercom.io wss://nexus-europe-websocket.intercom.io wss://nexus-australia-websocket.intercom.io https://uploads.intercomcdn.com https://tools.applemediaservices.com;
|
||||
font-src 'self' data: https://cdn.jsdelivr.net https://js.intercomcdn.com https://fonts.intercomcdn.com;
|
||||
form-action 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://getpocket.com/auth/authorize https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io;
|
||||
form-action 'self' ${process.env.NEXT_PUBLIC_SERVER_BASE_URL} https://getpocket.com/auth/authorize https://intercom.help https://api-iam.intercom.io https://api-iam.eu.intercom.io https://api-iam.au.intercom.io https://www.notion.so https://api.notion.com;
|
||||
frame-ancestors 'none';
|
||||
frame-src 'self' https://accounts.google.com https://platform.twitter.com https://www.youtube.com https://www.youtube-nocookie.com;
|
||||
manifest-src 'self';
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import Head from 'next/head'
|
||||
import { ErrorLayout } from '../components/templates/ErrorLayout'
|
||||
import { SettingsLayout } from '../components/templates/SettingsLayout'
|
||||
import { EmptyLayout } from '../components/templates/EmptyLayout'
|
||||
|
||||
export default function Custom404(): JSX.Element {
|
||||
return (
|
||||
|
|
@ -8,9 +8,9 @@ export default function Custom404(): JSX.Element {
|
|||
<Head>
|
||||
<title>Page Not Found</title>
|
||||
</Head>
|
||||
<SettingsLayout title="Page could not be found">
|
||||
<EmptyLayout title="Page could not be found">
|
||||
<ErrorLayout statusCode={404} message="This page could not be found." />
|
||||
</SettingsLayout>
|
||||
</EmptyLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ErrorLayout } from '../components/templates/ErrorLayout'
|
||||
import Head from 'next/head'
|
||||
import { SettingsLayout } from '../components/templates/SettingsLayout'
|
||||
import { EmptyLayout } from '../components/templates/EmptyLayout'
|
||||
|
||||
export default function Custom500(): JSX.Element {
|
||||
return (
|
||||
|
|
@ -8,9 +8,9 @@ export default function Custom500(): JSX.Element {
|
|||
<Head>
|
||||
<title>An unknown error occurred.</title>
|
||||
</Head>
|
||||
<SettingsLayout title="An unknown error occurred">
|
||||
<ErrorLayout statusCode={404} message="An unknown error occurred." />
|
||||
</SettingsLayout>
|
||||
<EmptyLayout title="An unknown error occurred">
|
||||
<ErrorLayout statusCode={404} message="An unknown error occurred." />
|
||||
</EmptyLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
231
packages/web/pages/settings/features/beta.tsx
Normal file
231
packages/web/pages/settings/features/beta.tsx
Normal file
|
|
@ -0,0 +1,231 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { Button } from '../../../components/elements/Button'
|
||||
import {
|
||||
Box,
|
||||
HStack,
|
||||
SpanBox,
|
||||
VStack,
|
||||
} from '../../../components/elements/LayoutPrimitives'
|
||||
import { StyledText } from '../../../components/elements/StyledText'
|
||||
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
|
||||
import { styled, theme } from '../../../components/tokens/stitches.config'
|
||||
import { updateEmailMutation } from '../../../lib/networking/mutations/updateEmailMutation'
|
||||
import { updateUserMutation } from '../../../lib/networking/mutations/updateUserMutation'
|
||||
import { updateUserProfileMutation } from '../../../lib/networking/mutations/updateUserProfileMutation'
|
||||
import { useGetLibraryItemsQuery } from '../../../lib/networking/queries/useGetLibraryItemsQuery'
|
||||
import { useGetViewerQuery } from '../../../lib/networking/queries/useGetViewerQuery'
|
||||
import { useValidateUsernameQuery } from '../../../lib/networking/queries/useValidateUsernameQuery'
|
||||
import { applyStoredTheme } from '../../../lib/themeUpdater'
|
||||
import { showErrorToast, showSuccessToast } from '../../../lib/toastHelpers'
|
||||
import { ConfirmationModal } from '../../../components/patterns/ConfirmationModal'
|
||||
import { ProgressBar } from '../../../components/elements/ProgressBar'
|
||||
import { emptyTrashMutation } from '../../../lib/networking/mutations/emptyTrashMutation'
|
||||
import { ProgressIndicator } from '@radix-ui/react-progress'
|
||||
import { Spinner } from 'phosphor-react'
|
||||
import { optInFeature } from '../../../lib/networking/mutations/optIntoFeatureMutation'
|
||||
|
||||
const ACCOUNT_LIMIT = 50_000
|
||||
|
||||
const StyledLabel = styled('label', {
|
||||
fontWeight: 600,
|
||||
fontSize: '16px',
|
||||
marginBottom: '5px',
|
||||
})
|
||||
|
||||
export default function Account(): JSX.Element {
|
||||
const { viewerData, isLoading, mutate } = useGetViewerQuery()
|
||||
const [pageLoading, setPageLoading] = useState(false)
|
||||
|
||||
const showSpinner = useMemo(() => {
|
||||
return isLoading || pageLoading
|
||||
}, [isLoading, pageLoading])
|
||||
|
||||
const requestFeatureAccess = useCallback(
|
||||
async (featureName: string) => {
|
||||
setPageLoading(true)
|
||||
const result = await optInFeature({ name: featureName })
|
||||
if (!result) {
|
||||
showErrorToast('Error opting into feature.')
|
||||
} else {
|
||||
showSuccessToast('Feature added')
|
||||
}
|
||||
mutate()
|
||||
setPageLoading(false)
|
||||
},
|
||||
[setPageLoading, mutate]
|
||||
)
|
||||
|
||||
const hasYouTube = useMemo(() => {
|
||||
return (
|
||||
(viewerData?.me?.features.indexOf('youtube-transcripts') ?? -1) !== -1
|
||||
)
|
||||
}, [viewerData])
|
||||
|
||||
applyStoredTheme()
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
<Toaster
|
||||
containerStyle={{
|
||||
top: '5rem',
|
||||
}}
|
||||
/>
|
||||
|
||||
<VStack
|
||||
css={{ width: '100%', height: '100%' }}
|
||||
distribution="start"
|
||||
alignment="center"
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
minWidth: '300px',
|
||||
maxWidth: '865px',
|
||||
}}
|
||||
>
|
||||
<VStack
|
||||
css={{
|
||||
padding: '24px',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
bg: '$grayBg',
|
||||
gap: '10px',
|
||||
borderRadius: '5px',
|
||||
}}
|
||||
>
|
||||
<StyledLabel>Enabled beta features</StyledLabel>
|
||||
{!showSpinner ? (
|
||||
<>
|
||||
{viewerData?.me?.features.map((feature) => {
|
||||
return (
|
||||
<StyledText
|
||||
key={`feature-${feature}`}
|
||||
style="footnote"
|
||||
css={{
|
||||
display: 'flex',
|
||||
gap: '5px',
|
||||
marginBlockStart: '0px',
|
||||
marginBlockEnd: '0px',
|
||||
}}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={true}
|
||||
disabled={true}
|
||||
></input>
|
||||
{feature}
|
||||
</StyledText>
|
||||
)
|
||||
})}
|
||||
|
||||
{!hasYouTube /* || !hasAISummaries || !hasDigest */ && (
|
||||
<StyledLabel css={{ mt: '25px' }}>
|
||||
Available beta features
|
||||
</StyledLabel>
|
||||
)}
|
||||
|
||||
<VStack css={{ gap: '15px' }}>
|
||||
{!hasYouTube && (
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText
|
||||
style="footnote"
|
||||
css={{ display: 'flex', gap: '5px' }}
|
||||
>
|
||||
- YouTube transcripts: nicely formatted documents
|
||||
generated from YouTube transcript data. Currently
|
||||
limited to videos under 30 minutes.
|
||||
</StyledText>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={(event) => {
|
||||
requestFeatureAccess('youtube-transcripts')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Request feature
|
||||
</Button>
|
||||
</VStack>
|
||||
)}
|
||||
|
||||
{/* <VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText
|
||||
style="footnote"
|
||||
css={{ display: 'flex', gap: '5px' }}
|
||||
>
|
||||
- AI Summaries: Short summaries of your newly saved
|
||||
articles
|
||||
</StyledText>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={(event) => {
|
||||
requestFeatureAccess('ai-summaries')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Request feature
|
||||
</Button>
|
||||
</VStack>
|
||||
|
||||
<VStack
|
||||
alignment="start"
|
||||
distribution="start"
|
||||
css={{ width: '100%' }}
|
||||
>
|
||||
<StyledText
|
||||
style="footnote"
|
||||
css={{ display: 'flex', gap: '5px' }}
|
||||
>
|
||||
- Daily digest: Every day we pick some of the items we
|
||||
think You will enjoy reading the most and create a daily
|
||||
digest of them.
|
||||
</StyledText>
|
||||
<Button
|
||||
style="ctaDarkYellow"
|
||||
onClick={(event) => {
|
||||
requestFeatureAccess('daily-digest')
|
||||
event.preventDefault()
|
||||
}}
|
||||
>
|
||||
Request feature
|
||||
</Button>
|
||||
</VStack> */}
|
||||
</VStack>
|
||||
</>
|
||||
) : (
|
||||
<HStack
|
||||
distribution="center"
|
||||
css={{
|
||||
width: '100%',
|
||||
svg: {
|
||||
animation: 'spin 1s infinite linear',
|
||||
'@keyframes spin': {
|
||||
from: {
|
||||
transform: 'scale(1) rotate(0deg)',
|
||||
},
|
||||
to: {
|
||||
transform: 'scale(1) rotate(360deg)',
|
||||
},
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Spinner size={16} />
|
||||
</HStack>
|
||||
)}
|
||||
</VStack>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -89,6 +89,9 @@ export default function Integrations(): JSX.Element {
|
|||
const pocketConnected = useMemo(() => {
|
||||
return integrations.find((i) => i.name == 'POCKET' && i.type == 'IMPORT')
|
||||
}, [integrations])
|
||||
const isConnected = (name: string) => {
|
||||
return integrations.find((i) => i.name == name)?.enabled
|
||||
}
|
||||
|
||||
const deleteIntegration = async (id: string) => {
|
||||
try {
|
||||
|
|
@ -110,17 +113,23 @@ export default function Integrations(): JSX.Element {
|
|||
}
|
||||
}
|
||||
|
||||
const redirectToPocket = (importItemState: ImportItemState) => {
|
||||
const redirectToIntegration = (
|
||||
name: string,
|
||||
importItemState?: ImportItemState
|
||||
) => {
|
||||
// create a form and submit it to the backend
|
||||
const form = document.createElement('form')
|
||||
form.method = 'POST'
|
||||
form.action = `${fetchEndpoint}/integration/pocket/auth`
|
||||
const input = document.createElement('input')
|
||||
input.type = 'hidden'
|
||||
input.name = 'state'
|
||||
input.value = importItemState
|
||||
form.appendChild(input)
|
||||
form.action = `${fetchEndpoint}/integration/${name.toLowerCase()}/auth`
|
||||
if (importItemState) {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'hidden'
|
||||
input.name = 'state'
|
||||
input.value = importItemState
|
||||
form.appendChild(input)
|
||||
}
|
||||
document.body.appendChild(form)
|
||||
|
||||
form.submit()
|
||||
}
|
||||
|
||||
|
|
@ -155,13 +164,41 @@ export default function Integrations(): JSX.Element {
|
|||
{ duration: 5000 }
|
||||
)
|
||||
} finally {
|
||||
router.replace('/settings/integrations')
|
||||
router.push('/settings/integrations')
|
||||
}
|
||||
}
|
||||
|
||||
const connectWithNotion = async () => {
|
||||
try {
|
||||
// get the token from query string
|
||||
const token = router.query.code as string
|
||||
await setIntegrationMutation({
|
||||
token,
|
||||
name: 'NOTION',
|
||||
type: 'EXPORT',
|
||||
enabled: false,
|
||||
})
|
||||
|
||||
showSuccessToast('Connected with Notion.')
|
||||
|
||||
router.push('/settings/integrations/notion')
|
||||
} catch (err) {
|
||||
showErrorToast(
|
||||
'There was an error connecting to Notion. Please try again.',
|
||||
{ duration: 5000 }
|
||||
)
|
||||
|
||||
router.push('/settings/integrations')
|
||||
}
|
||||
}
|
||||
|
||||
if (!router.isReady) return
|
||||
if (router.query.pocketToken && router.query.state && !pocketConnected) {
|
||||
connectToPocket()
|
||||
}
|
||||
if (router.query.code) {
|
||||
connectWithNotion()
|
||||
}
|
||||
}, [router])
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -210,7 +247,7 @@ export default function Integrations(): JSX.Element {
|
|||
action: () => {
|
||||
pocketConnected
|
||||
? deleteIntegration(pocketConnected.id)
|
||||
: redirectToPocket(ImportItemState.Unarchived)
|
||||
: redirectToIntegration('pocket', ImportItemState.Unarchived)
|
||||
},
|
||||
disabled: isImporting(pocketConnected),
|
||||
isDropdown: !pocketConnected,
|
||||
|
|
@ -218,18 +255,34 @@ export default function Integrations(): JSX.Element {
|
|||
{
|
||||
text: 'Import All',
|
||||
action: () => {
|
||||
redirectToPocket(ImportItemState.All)
|
||||
redirectToIntegration('pocket', ImportItemState.All)
|
||||
},
|
||||
},
|
||||
{
|
||||
text: 'Import Unarchived',
|
||||
action: () => {
|
||||
redirectToPocket(ImportItemState.Unarchived)
|
||||
redirectToIntegration('pocket', ImportItemState.Unarchived)
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: '/static/icons/notion.png',
|
||||
title: 'Notion',
|
||||
subText:
|
||||
'Notion is an all-in-one workspace. Use our Notion integration to sync your Omnivore items to Notion.',
|
||||
button: {
|
||||
text: isConnected('NOTION') ? 'Settings' : 'Connect',
|
||||
icon: <Link size={16} weight={'bold'} />,
|
||||
style: isConnected('NOTION') ? 'ctaWhite' : 'ctaDarkYellow',
|
||||
action: () => {
|
||||
isConnected('NOTION')
|
||||
? router.push('/settings/integrations/notion')
|
||||
: redirectToIntegration('NOTION')
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: '/static/icons/webhooks.svg',
|
||||
title: 'Webhooks',
|
||||
|
|
@ -258,7 +311,7 @@ export default function Integrations(): JSX.Element {
|
|||
},
|
||||
},
|
||||
])
|
||||
}, [pocketConnected, readwiseConnected, webhooks])
|
||||
}, [pocketConnected, readwiseConnected, webhooks, integrations])
|
||||
|
||||
return (
|
||||
<SettingsLayout>
|
||||
|
|
|
|||
197
packages/web/pages/settings/integrations/notion.tsx
Normal file
197
packages/web/pages/settings/integrations/notion.tsx
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Form,
|
||||
FormProps,
|
||||
Input,
|
||||
message,
|
||||
Space,
|
||||
Switch,
|
||||
} from 'antd'
|
||||
import 'antd/dist/antd.compact.css'
|
||||
import { CheckboxValueType } from 'antd/lib/checkbox/Group'
|
||||
import Image from 'next/image'
|
||||
import { useRouter } from 'next/router'
|
||||
import { useEffect, useMemo } from 'react'
|
||||
import { HStack, VStack } from '../../../components/elements/LayoutPrimitives'
|
||||
import { PageMetaData } from '../../../components/patterns/PageMetaData'
|
||||
import { Beta } from '../../../components/templates/Beta'
|
||||
import { Header } from '../../../components/templates/settings/SettingsTable'
|
||||
import { SettingsLayout } from '../../../components/templates/SettingsLayout'
|
||||
import { deleteIntegrationMutation } from '../../../lib/networking/mutations/deleteIntegrationMutation'
|
||||
import { setIntegrationMutation } from '../../../lib/networking/mutations/setIntegrationMutation'
|
||||
import { useGetIntegrationsQuery } from '../../../lib/networking/queries/useGetIntegrationsQuery'
|
||||
import { applyStoredTheme } from '../../../lib/themeUpdater'
|
||||
import { showSuccessToast } from '../../../lib/toastHelpers'
|
||||
|
||||
type FieldType = {
|
||||
parentPageId?: string
|
||||
parentDatabaseId?: string
|
||||
enabled: boolean
|
||||
properties?: string[]
|
||||
}
|
||||
|
||||
export default function Notion(): JSX.Element {
|
||||
applyStoredTheme()
|
||||
|
||||
const router = useRouter()
|
||||
const { integrations, revalidate } = useGetIntegrationsQuery()
|
||||
const notion = useMemo(() => {
|
||||
return integrations.find((i) => i.name == 'NOTION' && i.type == 'EXPORT')
|
||||
}, [integrations])
|
||||
|
||||
const [form] = Form.useForm<FieldType>()
|
||||
const [messageApi, contextHolder] = message.useMessage()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
parentPageId: notion?.settings?.parentPageId,
|
||||
parentDatabaseId: notion?.settings?.parentDatabaseId,
|
||||
enabled: notion?.enabled,
|
||||
properties: notion?.settings?.properties,
|
||||
})
|
||||
}, [form, notion])
|
||||
|
||||
const deleteNotion = async () => {
|
||||
if (!notion) {
|
||||
throw new Error('Notion integration not found')
|
||||
}
|
||||
|
||||
await deleteIntegrationMutation(notion.id)
|
||||
showSuccessToast('Notion integration disconnected successfully.')
|
||||
|
||||
revalidate()
|
||||
router.push('/settings/integrations')
|
||||
}
|
||||
|
||||
const updateNotion = async (values: FieldType) => {
|
||||
if (!notion) {
|
||||
throw new Error('Notion integration not found')
|
||||
}
|
||||
|
||||
await setIntegrationMutation({
|
||||
id: notion.id,
|
||||
name: notion.name,
|
||||
type: notion.type,
|
||||
token: notion.token,
|
||||
enabled: values.enabled,
|
||||
settings: values,
|
||||
})
|
||||
}
|
||||
|
||||
const onFinish: FormProps<FieldType>['onFinish'] = async (values) => {
|
||||
try {
|
||||
await updateNotion(values)
|
||||
|
||||
revalidate()
|
||||
messageApi.success('Notion settings updated successfully.')
|
||||
} catch (error) {
|
||||
messageApi.error('There was an error updating Notion settings.')
|
||||
}
|
||||
}
|
||||
|
||||
const onFinishFailed: FormProps<FieldType>['onFinishFailed'] = (
|
||||
errorInfo
|
||||
) => {
|
||||
console.log('Failed:', errorInfo)
|
||||
}
|
||||
|
||||
const onDataChange = (value: Array<CheckboxValueType>) => {
|
||||
form.setFieldsValue({ properties: value.map((v) => v.toString()) })
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{contextHolder}
|
||||
<PageMetaData title="Notion" path="/integrations/notion" />
|
||||
<SettingsLayout>
|
||||
<VStack
|
||||
css={{
|
||||
margin: '0 auto',
|
||||
width: '80%',
|
||||
}}
|
||||
>
|
||||
<HStack
|
||||
css={{
|
||||
width: '100%',
|
||||
pb: '$2',
|
||||
borderBottom: '1px solid $utilityTextDefault',
|
||||
pr: '$1',
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src="/static/icons/notion.png"
|
||||
alt="Integration Image"
|
||||
width={75}
|
||||
height={75}
|
||||
/>
|
||||
<Header>Notion integration settings</Header>
|
||||
<Beta />
|
||||
</HStack>
|
||||
|
||||
{notion && (
|
||||
<div style={{ width: '100%', marginTop: '40px' }}>
|
||||
<Form
|
||||
labelCol={{ span: 6 }}
|
||||
wrapperCol={{ span: 8 }}
|
||||
labelAlign="left"
|
||||
form={form}
|
||||
onFinish={onFinish}
|
||||
onFinishFailed={onFinishFailed}
|
||||
>
|
||||
<Form.Item<FieldType>
|
||||
label="Notion Page Id"
|
||||
name="parentPageId"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: 'Please input your Notion Page Id!',
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Notion Database Id"
|
||||
name="parentDatabaseId"
|
||||
hidden
|
||||
>
|
||||
<Input disabled />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Automatic Sync"
|
||||
name="enabled"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<FieldType>
|
||||
label="Properties to Export"
|
||||
name="properties"
|
||||
>
|
||||
<Checkbox.Group onChange={onDataChange}>
|
||||
<Checkbox value="highlights">Highlights</Checkbox>
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
<Button type="primary" htmlType="submit">
|
||||
Save
|
||||
</Button>
|
||||
<Button type="primary" danger onClick={deleteNotion}>
|
||||
Disconnect
|
||||
</Button>
|
||||
</Space>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
)}
|
||||
</VStack>
|
||||
</SettingsLayout>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useEffect, useCallback } from 'react'
|
||||
import { Button } from '../components/elements/Button'
|
||||
import { HStack } from '../components/elements/LayoutPrimitives'
|
||||
import { SettingsLayout } from '../components/templates/SettingsLayout'
|
||||
import { EmptyLayout } from '../components/templates/EmptyLayout'
|
||||
import { setupAnalytics } from '../lib/analytics'
|
||||
|
||||
export default function Support(): JSX.Element {
|
||||
|
|
@ -18,7 +18,7 @@ export default function Support(): JSX.Element {
|
|||
}, [initAnalytics])
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Support">
|
||||
<EmptyLayout title="Support">
|
||||
<HStack
|
||||
alignment="center"
|
||||
distribution="end"
|
||||
|
|
@ -46,6 +46,6 @@ export default function Support(): JSX.Element {
|
|||
{'Open Chat Window'}
|
||||
</Button>
|
||||
</HStack>
|
||||
</SettingsLayout>
|
||||
</EmptyLayout>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { useRouter } from 'next/router'
|
||||
import { TermsAndConditions } from '../components/templates/TermsAndConditions'
|
||||
import { SettingsLayout } from '../components/templates/SettingsLayout'
|
||||
import { EmptyLayout } from '../components/templates/EmptyLayout'
|
||||
|
||||
export default function Terms(): JSX.Element {
|
||||
const router = useRouter()
|
||||
|
|
@ -11,9 +11,9 @@ export default function Terms(): JSX.Element {
|
|||
return <TermsAndConditions />
|
||||
} else {
|
||||
return (
|
||||
<SettingsLayout title="Terms and Conditions">
|
||||
<EmptyLayout title="Terms and Conditions">
|
||||
<TermsAndConditions />
|
||||
</SettingsLayout>
|
||||
</EmptyLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
BIN
packages/web/public/static/icons/notion.png
Normal file
BIN
packages/web/public/static/icons/notion.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
|
|
@ -610,3 +610,58 @@
|
|||
white-space: pre-wrap;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.is-sticky {
|
||||
position: fixed;
|
||||
right: 5px;
|
||||
bottom: 5px;
|
||||
top: auto;
|
||||
left: auto;
|
||||
z-index: 10;
|
||||
max-width: 400px;
|
||||
max-height: 222px;
|
||||
width: 400px;
|
||||
height: 222px;
|
||||
animation-name: fadeInUp;
|
||||
animation-duration: 0.5s;
|
||||
animation-fill-mode: both;
|
||||
-webkit-animation-name: fadeInUp;
|
||||
-webkit-animation-duration: 0.5s;
|
||||
-webkit-animation-fill-mode: both;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 4px 4px rgba(33, 33, 33, 0.1) !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.is-sticky {
|
||||
max-width: 200px;
|
||||
max-height: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, 100%, 0);
|
||||
transform: translate3d(0, 100%, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeInUp {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translate3d(0, 100%, 0);
|
||||
transform: translate3d(0, 100%, 0);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: none;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
|
@ -2830,9 +2830,9 @@ fn.name@1.x.x:
|
|||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
follow-redirects@^1.14.0:
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
|
||||
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
|
||||
version "1.15.6"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
|
||||
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||
|
||||
formidable@^1.0.17:
|
||||
version "1.2.2"
|
||||
|
|
|
|||
370
yarn.lock
370
yarn.lock
|
|
@ -2428,6 +2428,11 @@
|
|||
dependencies:
|
||||
text-decoding "^1.0.0"
|
||||
|
||||
"@fastify/busboy@^2.0.0":
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d"
|
||||
integrity sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==
|
||||
|
||||
"@ffmpeg-installer/darwin-arm64@4.1.5":
|
||||
version "4.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz#b7b5c262dd96d1aea4807514e1cdcf6e11f82743"
|
||||
|
|
@ -2768,12 +2773,7 @@
|
|||
resolved "https://registry.yarnpkg.com/@google-cloud/projectify/-/projectify-4.0.0.tgz#d600e0433daf51b88c1fa95ac7f02e38e80a07be"
|
||||
integrity sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==
|
||||
|
||||
"@google-cloud/promisify@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-3.0.0.tgz#5cd6941fc30c4acac18051706aa5af96069bd3e3"
|
||||
integrity sha512-91ArYvRgXWb73YvEOBMmOcJc0bDRs5yiVHnqkwoG0f3nm7nZuipllz6e7BvFESBvjkDTBC0zMD8QxedUwNLc1A==
|
||||
|
||||
"@google-cloud/promisify@^3.0.1":
|
||||
"@google-cloud/promisify@^3.0.0", "@google-cloud/promisify@^3.0.1":
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@google-cloud/promisify/-/promisify-3.0.1.tgz#8d724fb280f47d1ff99953aee0c1669b25238c2e"
|
||||
integrity sha512-z1CjRjtQyBOYL+5Qr9DdYIfrdLBe746jRTYfaYU6MeXkqp7UfYs/jX16lFFVzZ7PGEJvqZNqYUEtb1mvDww4pA==
|
||||
|
|
@ -2830,25 +2830,25 @@
|
|||
uuid "^8.0.0"
|
||||
|
||||
"@google-cloud/storage@^7.0.1":
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.0.1.tgz#38c267bb8377d442066d4eccb4f942f58d119476"
|
||||
integrity sha512-YBJ8HaDZvbeVDgEGWuC6sCsfZNCooVfKg1J+CJ4iXwRejIWbKFjl8laWz8w+/+ucJHM9qOdGkB95Q/mhh2CX/A==
|
||||
version "7.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@google-cloud/storage/-/storage-7.8.0.tgz#78b41d575c05b35a6dae2dea0e5c2b09bc5ec532"
|
||||
integrity sha512-4q8rKdLp35z8msAtrhr0pbos7BeD8T0tr6rMbBINewp9cfrwj7ROIElVwBluU8fZ596OvwQcjb6QCyBzTmkMRQ==
|
||||
dependencies:
|
||||
"@google-cloud/paginator" "^3.0.7"
|
||||
"@google-cloud/projectify" "^3.0.0"
|
||||
"@google-cloud/promisify" "^3.0.0"
|
||||
"@google-cloud/paginator" "^5.0.0"
|
||||
"@google-cloud/projectify" "^4.0.0"
|
||||
"@google-cloud/promisify" "^4.0.0"
|
||||
abort-controller "^3.0.0"
|
||||
async-retry "^1.3.3"
|
||||
compressible "^2.0.12"
|
||||
duplexify "^4.0.0"
|
||||
duplexify "^4.1.3"
|
||||
ent "^2.2.0"
|
||||
fast-xml-parser "^4.2.2"
|
||||
fast-xml-parser "^4.3.0"
|
||||
gaxios "^6.0.2"
|
||||
google-auth-library "^9.0.0"
|
||||
google-auth-library "^9.6.3"
|
||||
mime "^3.0.0"
|
||||
mime-types "^2.0.8"
|
||||
p-limit "^3.0.1"
|
||||
retry-request "^6.0.0"
|
||||
retry-request "^7.0.0"
|
||||
teeny-request "^9.0.0"
|
||||
uuid "^8.0.0"
|
||||
|
||||
|
|
@ -3978,6 +3978,14 @@
|
|||
"@nodelib/fs.scandir" "2.1.3"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@notionhq/client@^2.2.14":
|
||||
version "2.2.14"
|
||||
resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-2.2.14.tgz#6807ec27ee89584529abfd28d058b2661f828b74"
|
||||
integrity sha512-oqUefZtCiJPCX+74A1Os9OVTef3fSnVWe2eVQtU1HJSD+nsfxfhwvDKnzJTh2Tw1ZHKLxpieHB/nzGdY+Uo12A==
|
||||
dependencies:
|
||||
"@types/node-fetch" "^2.5.10"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@npmcli/arborist@^5.6.3":
|
||||
version "5.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@npmcli/arborist/-/arborist-5.6.3.tgz#40810080272e097b4a7a4f56108f4a31638a9874"
|
||||
|
|
@ -6084,11 +6092,6 @@
|
|||
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.3.tgz#1185726610acc37317ddab11c3c7f9066966bd20"
|
||||
integrity sha512-O3uyB/JbkAEMZaP3YqyHH7TMnex7tWyCbCI4EfJdOCoN6HIhqdJBWTM6aCCiWQ/5f5wxjgU735QAIpJbjDvmzg==
|
||||
|
||||
"@sqltools/formatter@^1.2.5":
|
||||
version "1.2.5"
|
||||
resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
|
||||
integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
|
||||
|
||||
"@stitches/react@^1.2.5":
|
||||
version "1.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@stitches/react/-/react-1.2.8.tgz#954f8008be8d9c65c4e58efa0937f32388ce3a38"
|
||||
|
|
@ -7486,10 +7489,10 @@
|
|||
dependencies:
|
||||
"@types/express" "*"
|
||||
|
||||
"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.7":
|
||||
version "4.17.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.17.tgz#01d5437f6ef9cfa8668e616e13c2f2ac9a491ae4"
|
||||
integrity sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==
|
||||
"@types/express@*", "@types/express@^4.17.13", "@types/express@^4.17.14", "@types/express@^4.17.21", "@types/express@^4.17.7":
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.33"
|
||||
|
|
@ -7506,16 +7509,6 @@
|
|||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/express@^4.17.21":
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.33"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/filesystem@*":
|
||||
version "0.0.32"
|
||||
resolved "https://registry.yarnpkg.com/@types/filesystem/-/filesystem-0.0.32.tgz#307df7cc084a2293c3c1a31151b178063e0a8edf"
|
||||
|
|
@ -7881,6 +7874,14 @@
|
|||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/node-fetch@^2.5.10", "@types/node-fetch@^2.6.4":
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
|
||||
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node-fetch@^2.5.7":
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.1.tgz#8f127c50481db65886800ef496f20bbf15518975"
|
||||
|
|
@ -7889,14 +7890,6 @@
|
|||
"@types/node" "*"
|
||||
form-data "^3.0.0"
|
||||
|
||||
"@types/node-fetch@^2.6.4":
|
||||
version "2.6.11"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.11.tgz#9b39b78665dae0e82a08f02f4967d62c66f95d24"
|
||||
integrity sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
form-data "^4.0.0"
|
||||
|
||||
"@types/node-fetch@^2.6.6":
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/node-fetch/-/node-fetch-2.6.7.tgz#a1abe2ce24228b58ad97f99480fdcf9bbc6ab16d"
|
||||
|
|
@ -7932,13 +7925,6 @@
|
|||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/node@^20.11.0":
|
||||
version "20.11.24"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.24.tgz#cc207511104694e84e9fb17f9a0c4c42d4517792"
|
||||
integrity sha512-Kza43ewS3xoLgCEpQrsT+xRo/EJej1y0kVYGiLFE1NEODXGzTfwiC6tXTLMQskn1X4/Rjlh0MQUvx9W+L9long==
|
||||
dependencies:
|
||||
undici-types "~5.26.4"
|
||||
|
||||
"@types/nodemailer@^6.4.4":
|
||||
version "6.4.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b"
|
||||
|
|
@ -8105,6 +8091,16 @@
|
|||
"@types/tough-cookie" "*"
|
||||
form-data "^2.5.0"
|
||||
|
||||
"@types/request@^2.48.8":
|
||||
version "2.48.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.12.tgz#0f590f615a10f87da18e9790ac94c29ec4c5ef30"
|
||||
integrity sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==
|
||||
dependencies:
|
||||
"@types/caseless" "*"
|
||||
"@types/node" "*"
|
||||
"@types/tough-cookie" "*"
|
||||
form-data "^2.5.0"
|
||||
|
||||
"@types/retry@0.12.0":
|
||||
version "0.12.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/retry/-/retry-0.12.0.tgz#2b35eccfcee7d38cd72ad99232fbd58bffb3c84d"
|
||||
|
|
@ -8172,6 +8168,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-2.0.1.tgz#24134738ba3107237d6a783e054a54773e739f81"
|
||||
integrity sha512-xdnAw2nFqomkaL0QdtEk0t7yz26UkaVPl4v1pYJvtE1T0fmfQEH3JaxErEhGByEAl3zUZrkNBlneuJp0WJGqEA==
|
||||
|
||||
"@types/showdown@^2.0.6":
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/@types/showdown/-/showdown-2.0.6.tgz#3d7affd5f971b4a17783ec2b23b4ad3b97477b7e"
|
||||
integrity sha512-pTvD/0CIeqe4x23+YJWlX2gArHa8G0J0Oh6GKaVXV7TAeickpkkZiNOgFcFcmLQ5lB/K0qBJL1FtRYltBfbGCQ==
|
||||
|
||||
"@types/sinon-chai@^3.2.8":
|
||||
version "3.2.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/sinon-chai/-/sinon-chai-3.2.8.tgz#5871d09ab50d671d8e6dd72e9073f8e738ac61dc"
|
||||
|
|
@ -9466,11 +9467,6 @@ app-root-path@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.0.0.tgz#210b6f43873227e18a4b810a032283311555d5ad"
|
||||
integrity sha512-qMcx+Gy2UZynHjOHOIXPNvpf+9cjvk3cWrBBK7zg4gH9+clobJRb9NGzcT7mQTcV/6Gm/1WelUtqxVXnNlrwcw==
|
||||
|
||||
app-root-path@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86"
|
||||
integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==
|
||||
|
||||
apparatus@^0.0.10:
|
||||
version "0.0.10"
|
||||
resolved "https://registry.yarnpkg.com/apparatus/-/apparatus-0.0.10.tgz#81ea756772ada77863db54ceee8202c109bdca3e"
|
||||
|
|
@ -10564,13 +10560,13 @@ bn.js@^5.2.1:
|
|||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
|
||||
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
|
||||
|
||||
body-parser@1.20.1, body-parser@^1.18.3, body-parser@^1.19.0:
|
||||
version "1.20.1"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.1.tgz#b1812a8912c195cd371a3ee5e66faa2338a5c668"
|
||||
integrity sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==
|
||||
body-parser@1.20.2, body-parser@^1.18.3, body-parser@^1.19.0:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.4"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
|
|
@ -10578,7 +10574,7 @@ body-parser@1.20.1, body-parser@^1.18.3, body-parser@^1.19.0:
|
|||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.11.0"
|
||||
raw-body "2.5.1"
|
||||
raw-body "2.5.2"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
|
|
@ -12174,10 +12170,10 @@ content-disposition@0.5.4:
|
|||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b"
|
||||
integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
|
||||
content-type@~1.0.4, content-type@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
||||
conventional-changelog-angular@6.0.0:
|
||||
version "6.0.0"
|
||||
|
|
@ -12929,7 +12925,7 @@ dateformat@^3.0.0, dateformat@^3.0.3:
|
|||
resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae"
|
||||
integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==
|
||||
|
||||
dayjs@1.x, dayjs@^1.10.4, dayjs@^1.11.7, dayjs@^1.11.9:
|
||||
dayjs@1.x, dayjs@^1.10.4, dayjs@^1.11.7:
|
||||
version "1.11.10"
|
||||
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.10.tgz#68acea85317a6e164457d6d6947564029a6a16a0"
|
||||
integrity sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==
|
||||
|
|
@ -13660,11 +13656,6 @@ dotenv@^16.0.1:
|
|||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.1.tgz#8f8f9d94876c35dac989876a5d3a82a267fdce1d"
|
||||
integrity sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==
|
||||
|
||||
dotenv@^16.0.3:
|
||||
version "16.4.5"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
|
||||
integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
|
||||
|
||||
dotenv@^8.0.0, dotenv@^8.2.0:
|
||||
version "8.6.0"
|
||||
resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.6.0.tgz#061af664d19f7f4d8fc6e4ff9b584ce237adcb8b"
|
||||
|
|
@ -13718,15 +13709,15 @@ duplexify@^3.4.2, duplexify@^3.6.0:
|
|||
readable-stream "^2.0.0"
|
||||
stream-shift "^1.0.0"
|
||||
|
||||
duplexify@^4.0.0, duplexify@^4.1.1:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.2.tgz#18b4f8d28289132fa0b9573c898d9f903f81c7b0"
|
||||
integrity sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==
|
||||
duplexify@^4.0.0, duplexify@^4.1.1, duplexify@^4.1.3:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-4.1.3.tgz#a07e1c0d0a2c001158563d32592ba58bddb0236f"
|
||||
integrity sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==
|
||||
dependencies:
|
||||
end-of-stream "^1.4.1"
|
||||
inherits "^2.0.3"
|
||||
readable-stream "^3.1.1"
|
||||
stream-shift "^1.0.0"
|
||||
stream-shift "^1.0.2"
|
||||
|
||||
dynamic-dedupe@^0.3.0:
|
||||
version "0.3.0"
|
||||
|
|
@ -14214,13 +14205,14 @@ es-to-primitive@^1.2.1:
|
|||
is-date-object "^1.0.1"
|
||||
is-symbol "^1.0.2"
|
||||
|
||||
es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@~0.10.14:
|
||||
version "0.10.62"
|
||||
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5"
|
||||
integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==
|
||||
es5-ext@^0.10.35, es5-ext@^0.10.50, es5-ext@^0.10.62, es5-ext@~0.10.14:
|
||||
version "0.10.64"
|
||||
resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714"
|
||||
integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==
|
||||
dependencies:
|
||||
es6-iterator "^2.0.3"
|
||||
es6-symbol "^3.1.3"
|
||||
esniff "^2.0.1"
|
||||
next-tick "^1.1.0"
|
||||
|
||||
es5-shim@^4.5.13:
|
||||
|
|
@ -14661,6 +14653,16 @@ eslint@^8.6.0:
|
|||
text-table "^0.2.0"
|
||||
v8-compile-cache "^2.0.3"
|
||||
|
||||
esniff@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308"
|
||||
integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==
|
||||
dependencies:
|
||||
d "^1.0.1"
|
||||
es5-ext "^0.10.62"
|
||||
event-emitter "^0.3.5"
|
||||
type "^2.7.2"
|
||||
|
||||
espree@^9.0.0:
|
||||
version "9.4.1"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.4.1.tgz#51d6092615567a2c2cff7833445e37c28c0065bd"
|
||||
|
|
@ -14961,13 +14963,13 @@ express-rate-limit@^6.3.0:
|
|||
integrity sha512-8+UpWtQY25lJaa4+3WxDBGDcAu4atcTruSs3QSL5VPEplYy6kmk84wutG9rUkkK5LmMQQ7TFHWLZYITwVNbbEg==
|
||||
|
||||
express@^4.16.4, express@^4.17.1, express@^4.18.2:
|
||||
version "4.18.2"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.2.tgz#3fabe08296e930c796c19e3c516979386ba9fd59"
|
||||
integrity sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==
|
||||
version "4.18.3"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4"
|
||||
integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.20.1"
|
||||
body-parser "1.20.2"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.5.0"
|
||||
|
|
@ -15208,10 +15210,10 @@ fast-text-encoding@^1.0.0, fast-text-encoding@^1.0.3:
|
|||
resolved "https://registry.yarnpkg.com/fast-text-encoding/-/fast-text-encoding-1.0.3.tgz#ec02ac8e01ab8a319af182dae2681213cfe9ce53"
|
||||
integrity sha512-dtm4QZH9nZtcDt8qJiOH9fcQd1NAgi+K1O2DbE6GG1PPCK/BWfOH3idCTRQ4ImXRUOyopDEgDEnVEE7Y/2Wrig==
|
||||
|
||||
fast-xml-parser@^4.2.2:
|
||||
version "4.2.7"
|
||||
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.2.7.tgz#871f2ca299dc4334b29f8da3658c164e68395167"
|
||||
integrity sha512-J8r6BriSLO1uj2miOk1NW0YVm8AGOOu3Si2HQp/cSmo6EA4m3fcwu2WKjJ4RK9wMLBtg69y1kS8baDiQBR41Ig==
|
||||
fast-xml-parser@^4.2.2, fast-xml-parser@^4.3.0:
|
||||
version "4.3.5"
|
||||
resolved "https://registry.yarnpkg.com/fast-xml-parser/-/fast-xml-parser-4.3.5.tgz#e2f2a2ae8377e9c3dc321b151e58f420ca7e5ccc"
|
||||
integrity sha512-sWvP1Pl8H03B8oFJpFR3HE31HUfwtX7Rlf9BNsvdpujD4n7WMhfmu8h9wOV2u+c1k0ZilTADhPqypzx2J690ZQ==
|
||||
dependencies:
|
||||
strnum "^1.0.5"
|
||||
|
||||
|
|
@ -15549,15 +15551,10 @@ fn.name@1.x.x:
|
|||
resolved "https://registry.yarnpkg.com/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc"
|
||||
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
|
||||
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.0:
|
||||
version "1.15.4"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.4.tgz#cdc7d308bf6493126b17ea2191ea0ccf3e535adf"
|
||||
integrity sha512-Cr4D/5wlrb0z9dgERpUL3LrmPKVDsETIJhaCMeDfuFYcqa5bldGV6wBsAN6X/vxlXQtFBMrXdXxdL8CbDTGniw==
|
||||
|
||||
follow-redirects@^1.15.4:
|
||||
version "1.15.5"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.5.tgz#54d4d6d062c0fa7d9d17feb008461550e3ba8020"
|
||||
integrity sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==
|
||||
follow-redirects@^1.0.0, follow-redirects@^1.14.4, follow-redirects@^1.14.8, follow-redirects@^1.14.9, follow-redirects@^1.15.0, follow-redirects@^1.15.4:
|
||||
version "1.15.6"
|
||||
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.6.tgz#7f815c0cda4249c74ff09e95ef97c23b5fd0399b"
|
||||
integrity sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==
|
||||
|
||||
for-each@^0.3.3:
|
||||
version "0.3.3"
|
||||
|
|
@ -15973,20 +15970,10 @@ gaxios@^5.0.0, gaxios@^5.0.1:
|
|||
is-stream "^2.0.0"
|
||||
node-fetch "^2.6.7"
|
||||
|
||||
gaxios@^6.0.0, gaxios@^6.0.2:
|
||||
version "6.0.4"
|
||||
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.0.4.tgz#e8a2145653b5bad7e3cf358e2a9819160e8e6fa7"
|
||||
integrity sha512-mwKfHJn7f3pLRfahdEPNyvygXRwjwgsgDPaIIoBRIDkgP4SFyezkYGWQ2aLCfrAnzimSrP+mAg1aSUj5gidXvw==
|
||||
dependencies:
|
||||
extend "^3.0.2"
|
||||
https-proxy-agent "^7.0.1"
|
||||
is-stream "^2.0.0"
|
||||
node-fetch "^2.6.9"
|
||||
|
||||
gaxios@^6.0.3:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.1.0.tgz#8ab08adbf9cc600368a57545f58e004ccf831ccb"
|
||||
integrity sha512-EIHuesZxNyIkUGcTQKQPMICyOpDD/bi+LJIJx+NLsSGmnS7N+xCLRX5bi4e9yAu9AlSZdVq+qlyWWVuTh/483w==
|
||||
gaxios@^6.0.0, gaxios@^6.0.2, gaxios@^6.0.3, gaxios@^6.1.1:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gaxios/-/gaxios-6.3.0.tgz#5cd858de47c6560caaf0f99bb5d89c5bdfbe9034"
|
||||
integrity sha512-p+ggrQw3fBwH2F5N/PAI4k/G/y1art5OxKpb2J2chwNNHM4hHuAOtivjPuirMF4KNKwTTUal/lPfL2+7h2mEcg==
|
||||
dependencies:
|
||||
extend "^3.0.2"
|
||||
https-proxy-agent "^7.0.1"
|
||||
|
|
@ -16025,6 +16012,14 @@ gcp-metadata@^6.0.0:
|
|||
gaxios "^6.0.0"
|
||||
json-bigint "^1.0.0"
|
||||
|
||||
gcp-metadata@^6.1.0:
|
||||
version "6.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gcp-metadata/-/gcp-metadata-6.1.0.tgz#9b0dd2b2445258e7597f2024332d20611cbd6b8c"
|
||||
integrity sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==
|
||||
dependencies:
|
||||
gaxios "^6.0.0"
|
||||
json-bigint "^1.0.0"
|
||||
|
||||
gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
|
|
@ -16329,7 +16324,7 @@ glob@7.2.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glo
|
|||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
glob@^10.2.2, glob@^10.3.10:
|
||||
glob@^10.2.2:
|
||||
version "10.3.10"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
|
||||
integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
|
||||
|
|
@ -16531,18 +16526,17 @@ google-auth-library@^8.0.1, google-auth-library@^8.0.2:
|
|||
jws "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
google-auth-library@^9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.0.0.tgz#b159d22464c679a6a25cb46d48a4ac97f9f426a2"
|
||||
integrity sha512-IQGjgQoVUAfOk6khqTVMLvWx26R+yPw9uLyb1MNyMQpdKiKt0Fd9sp4NWoINjyGHR8S3iw12hMTYK7O8J07c6Q==
|
||||
google-auth-library@^9.0.0, google-auth-library@^9.6.3:
|
||||
version "9.6.3"
|
||||
resolved "https://registry.yarnpkg.com/google-auth-library/-/google-auth-library-9.6.3.tgz#add8935bc5b842a8e80f84fef2b5ed9febb41d48"
|
||||
integrity sha512-4CacM29MLC2eT9Cey5GDVK4Q8t+MMp8+OEdOaqD9MG6b0dOyLORaaeJMPQ7EESVgm/+z5EKYyFLxgzBJlJgyHQ==
|
||||
dependencies:
|
||||
base64-js "^1.3.0"
|
||||
ecdsa-sig-formatter "^1.0.11"
|
||||
gaxios "^6.0.0"
|
||||
gcp-metadata "^6.0.0"
|
||||
gaxios "^6.1.1"
|
||||
gcp-metadata "^6.1.0"
|
||||
gtoken "^7.0.0"
|
||||
jws "^4.0.0"
|
||||
lru-cache "^6.0.0"
|
||||
|
||||
google-gax@^3.5.7:
|
||||
version "3.5.8"
|
||||
|
|
@ -19298,17 +19292,24 @@ jest@^27.4.5:
|
|||
import-local "^3.0.2"
|
||||
jest-cli "^27.5.1"
|
||||
|
||||
jintr@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/jintr/-/jintr-1.1.0.tgz#223a3b07f5e03d410cec6e715c537c8ad1e714c3"
|
||||
integrity sha512-Tu9wk3BpN2v+kb8yT6YBtue+/nbjeLFv4vvVC4PJ7oCidHKbifWhvORrAbQfxVIQZG+67am/mDagpiGSVtvrZg==
|
||||
dependencies:
|
||||
acorn "^8.8.0"
|
||||
|
||||
jose@^2.0.5:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.6.tgz#894ba19169af339d3911be933f913dd02fc57c7c"
|
||||
integrity sha512-FVoPY7SflDodE4lknJmbAHSUjLCzE2H1F6MS0RYKMQ8SR+lNccpMf8R4eqkNYyyUjR5qZReOzZo5C5YiHOCjjg==
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-2.0.7.tgz#3aabbaec70bff313c108b9406498a163737b16ba"
|
||||
integrity sha512-5hFWIigKqC+e/lRyQhfnirrAqUdIPMB7SJRqflJaO29dW7q5DFvH1XCSTmv6PQ6pb++0k6MJlLRoS0Wv4s38Wg==
|
||||
dependencies:
|
||||
"@panva/asn1.js" "^1.0.0"
|
||||
|
||||
jose@^4.10.4:
|
||||
version "4.13.1"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.13.1.tgz#449111bb5ab171db85c03f1bd2cb1647ca06db1c"
|
||||
integrity sha512-MSJQC5vXco5Br38mzaQKiq9mwt7lwj2eXpgpRyQYNHYt2lq1PjkWa7DLXX0WVcQLE9HhMh3jPiufS7fhJf+CLQ==
|
||||
version "4.15.5"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-4.15.5.tgz#6475d0f467ecd3c630a1b5dadd2735a7288df706"
|
||||
integrity sha512-jc7BFxgKPKi94uOvEmzlSWFFe2+vASyXaKUpdQKatWAESU2MWjDfFf0fdfc83CDKcA5QecabZeNLyfhe3yKNkg==
|
||||
|
||||
js-beautify@^1.13.0:
|
||||
version "1.14.0"
|
||||
|
|
@ -21726,36 +21727,17 @@ miller-rabin@^4.0.0:
|
|||
bn.js "^4.0.0"
|
||||
brorand "^1.0.1"
|
||||
|
||||
mime-db@1.44.0:
|
||||
version "1.44.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92"
|
||||
integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==
|
||||
|
||||
mime-db@1.49.0, "mime-db@>= 1.43.0 < 2":
|
||||
version "1.49.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
|
||||
integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
|
||||
|
||||
mime-db@1.52.0:
|
||||
version "1.52.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.0.8, mime-types@~2.1.24:
|
||||
version "2.1.32"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.32.tgz#1d00e89e7de7fe02008db61001d9e02852670fd5"
|
||||
integrity sha512-hJGaVS4G4c9TSMYh2n6SQAGrC4RnfU+daP8G7cSCmaqNjiOoUY0VHCMS42pxnQmVF1GWwFhbHWn3RIxCqTmZ9A==
|
||||
dependencies:
|
||||
mime-db "1.49.0"
|
||||
"mime-db@>= 1.43.0 < 2":
|
||||
version "1.49.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.49.0.tgz#f3dfde60c99e9cf3bc9701d687778f537001cbed"
|
||||
integrity sha512-CIc8j9URtOVApSFCQIF+VBkX1RwXp/oMMOrqdyXSBXq5RWNEsRfyj1kiRnQgmNXmHxPoFIxOroKA3zcU9P+nAA==
|
||||
|
||||
mime-types@^2.1.12, mime-types@~2.1.19:
|
||||
version "2.1.27"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f"
|
||||
integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==
|
||||
dependencies:
|
||||
mime-db "1.44.0"
|
||||
|
||||
mime-types@^2.1.27, mime-types@^2.1.30, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.34:
|
||||
mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@^2.1.30, mime-types@^2.1.31, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
|
|
@ -22081,11 +22063,6 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
|||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
mkdirp@^2.1.3:
|
||||
version "2.1.6"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-2.1.6.tgz#964fbcb12b2d8c5d6fbc62a963ac95a273e2cc19"
|
||||
integrity sha512-+hEnITedc8LAtIP9u3HJDFIdcLV2vXP33sqLLIzkv1Db1zO/1OxbvYf0Y1OC/S/Qo5dxHXepofhmxL02PsKe+A==
|
||||
|
||||
mkdirp@~0.3.5:
|
||||
version "0.3.5"
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7"
|
||||
|
|
@ -25534,10 +25511,10 @@ range-parser@^1.2.1, range-parser@~1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.5.1:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.1.tgz#fe1b1628b181b700215e5fd42389f98b71392857"
|
||||
integrity sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==
|
||||
raw-body@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
|
|
@ -26390,14 +26367,6 @@ read-pkg@^7.1.0:
|
|||
parse-json "^5.2.0"
|
||||
type-fest "^2.0.0"
|
||||
|
||||
read-yaml-file@^2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/read-yaml-file/-/read-yaml-file-2.1.0.tgz#c5866712db9ef5343b4d02c2413bada53c41c4a9"
|
||||
integrity sha512-UkRNRIwnhG+y7hpqnycCL/xbTk7+ia9VuVTC0S+zVbwd65DI9eUpRMfsWIGrCWxTU/mi+JW8cHQCrv+zfCbEPQ==
|
||||
dependencies:
|
||||
js-yaml "^4.0.0"
|
||||
strip-bom "^4.0.0"
|
||||
|
||||
read@1, read@^1.0.7, read@~1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/read/-/read-1.0.7.tgz#b3da19bd052431a97671d44a42634adf710b40c4"
|
||||
|
|
@ -26540,11 +26509,6 @@ reflect-metadata@^0.1.13:
|
|||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08"
|
||||
integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==
|
||||
|
||||
reflect-metadata@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.1.tgz#8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74"
|
||||
integrity sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==
|
||||
|
||||
reflect.getprototypeof@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.4.tgz#aaccbf41aca3821b87bb71d9dcbc7ad0ba50a3f3"
|
||||
|
|
@ -27075,6 +27039,15 @@ retry-request@^6.0.0:
|
|||
debug "^4.1.1"
|
||||
extend "^3.0.2"
|
||||
|
||||
retry-request@^7.0.0:
|
||||
version "7.0.2"
|
||||
resolved "https://registry.yarnpkg.com/retry-request/-/retry-request-7.0.2.tgz#60bf48cfb424ec01b03fca6665dee91d06dd95f3"
|
||||
integrity sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==
|
||||
dependencies:
|
||||
"@types/request" "^2.48.8"
|
||||
extend "^3.0.2"
|
||||
teeny-request "^9.0.0"
|
||||
|
||||
retry@0.13.1, retry@^0.13.1:
|
||||
version "0.13.1"
|
||||
resolved "https://registry.yarnpkg.com/retry/-/retry-0.13.1.tgz#185b1587acf67919d63b357349e03537b2484658"
|
||||
|
|
@ -28329,6 +28302,11 @@ stream-shift@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
|
||||
integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
|
||||
|
||||
stream-shift@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.3.tgz#85b8fab4d71010fc3ba8772e8046cc49b8a3864b"
|
||||
integrity sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==
|
||||
|
||||
streamsearch@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764"
|
||||
|
|
@ -29785,27 +29763,6 @@ typeorm-naming-strategies@^4.1.0:
|
|||
resolved "https://registry.yarnpkg.com/typeorm-naming-strategies/-/typeorm-naming-strategies-4.1.0.tgz#1ec6eb296c8d7b69bb06764d5b9083ff80e814a9"
|
||||
integrity sha512-vPekJXzZOTZrdDvTl1YoM+w+sUIfQHG4kZTpbFYoTsufyv9NIBRe4Q+PdzhEAFA2std3D9LZHEb1EjE9zhRpiQ==
|
||||
|
||||
typeorm@^0.3.19:
|
||||
version "0.3.20"
|
||||
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab"
|
||||
integrity sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==
|
||||
dependencies:
|
||||
"@sqltools/formatter" "^1.2.5"
|
||||
app-root-path "^3.1.0"
|
||||
buffer "^6.0.3"
|
||||
chalk "^4.1.2"
|
||||
cli-highlight "^2.1.11"
|
||||
dayjs "^1.11.9"
|
||||
debug "^4.3.4"
|
||||
dotenv "^16.0.3"
|
||||
glob "^10.3.10"
|
||||
mkdirp "^2.1.3"
|
||||
reflect-metadata "^0.2.1"
|
||||
sha.js "^2.4.11"
|
||||
tslib "^2.5.0"
|
||||
uuid "^9.0.0"
|
||||
yargs "^17.6.2"
|
||||
|
||||
typeorm@^0.3.4:
|
||||
version "0.3.7"
|
||||
resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.7.tgz#5776ed5058f0acb75d64723b39ff458d21de64c1"
|
||||
|
|
@ -29844,11 +29801,6 @@ typescript@^4.4.4:
|
|||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a"
|
||||
integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==
|
||||
|
||||
typescript@^5.3.3:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.3.3.tgz#b3ce6ba258e72e6305ba66f5c9b452aaee3ffe37"
|
||||
integrity sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==
|
||||
|
||||
ua-parser-js@^0.7.30:
|
||||
version "0.7.33"
|
||||
resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.33.tgz#1d04acb4ccef9293df6f70f2c3d22f3030d8b532"
|
||||
|
|
@ -29937,6 +29889,13 @@ undici@^4.9.3:
|
|||
resolved "https://registry.yarnpkg.com/undici/-/undici-4.14.1.tgz#7633b143a8a10d6d63335e00511d071e8d52a1d9"
|
||||
integrity sha512-WJ+g+XqiZcATcBaUeluCajqy4pEDcQfK1vy+Fo+bC4/mqXI9IIQD/XWHLS70fkGUT6P52Drm7IFslO651OdLPQ==
|
||||
|
||||
undici@^5.19.1:
|
||||
version "5.28.3"
|
||||
resolved "https://registry.yarnpkg.com/undici/-/undici-5.28.3.tgz#a731e0eff2c3fcfd41c1169a869062be222d1e5b"
|
||||
integrity sha512-3ItfzbrhDlINjaP0duwnNsKpDQk3acHI3gVJ1z4fmwMK31k5G9OVIAMLSIaP6w4FaGkaAkN6zaQO9LUvZ1t7VA==
|
||||
dependencies:
|
||||
"@fastify/busboy" "^2.0.0"
|
||||
|
||||
unfetch@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/unfetch/-/unfetch-4.2.0.tgz#7e21b0ef7d363d8d9af0fb929a5555f6ef97a3be"
|
||||
|
|
@ -31597,6 +31556,23 @@ yocto-queue@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||
|
||||
youtubei.js@^9.1.0:
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/youtubei.js/-/youtubei.js-9.1.0.tgz#bcf154c9fa21d3c8c1d00a5e10360d0a065c660e"
|
||||
integrity sha512-C5GBJ4LgnS6vGAUkdIdQNOFFb5EZ1p3xBvUELNXmIG3Idr6vxWrKNBNy8ClZT3SuDVXaAJqDgF9b5jvY8lNKcg==
|
||||
dependencies:
|
||||
jintr "^1.1.0"
|
||||
tslib "^2.5.0"
|
||||
undici "^5.19.1"
|
||||
|
||||
youtubei@^1.3.4:
|
||||
version "1.3.4"
|
||||
resolved "https://registry.yarnpkg.com/youtubei/-/youtubei-1.3.4.tgz#b9761e33dcc6e0a9569e6628ba1fc48c729636f0"
|
||||
integrity sha512-xN6p2oddcTpreF/ojU2mChwdiUlV+TwwUL6xgP6lXRuxeGS5MokM1tzRdXCgIpxkzYYNNAWpt7xvPuAUQM0PCg==
|
||||
dependencies:
|
||||
node-fetch "2.6.7"
|
||||
protobufjs "7.2.4"
|
||||
|
||||
yup@^0.31.0:
|
||||
version "0.31.1"
|
||||
resolved "https://registry.yarnpkg.com/yup/-/yup-0.31.1.tgz#0954cb181161f397b804346037a04f8a4b31599e"
|
||||
|
|
|
|||
Loading…
Reference in a new issue