summaryrefslogtreecommitdiff
path: root/Providers
diff options
context:
space:
mode:
authorRuben Beltran del Rio <git@r.bdr.sh>2025-09-08 10:38:39 +0200
committerRuben Beltran del Rio <git@r.bdr.sh>2025-09-08 10:38:39 +0200
commita5e25cc05e5e33e114331cc826211c89adbcfa6a (patch)
tree2ea647b5117e5c3b530ec66d4caafd04969e0fbe /Providers
parent0f26c5c3b3e606c89724067d4785323de3fee751 (diff)
Reorganize code, WIP save for tag handling
Diffstat (limited to 'Providers')
-rw-r--r--Providers/QSDeliciousAPIProvider.h25
-rw-r--r--Providers/QSDeliciousAPIProvider.m193
-rw-r--r--Providers/QSLinkdingProvider.h16
-rw-r--r--Providers/QSLinkdingProvider.m192
4 files changed, 426 insertions, 0 deletions
diff --git a/Providers/QSDeliciousAPIProvider.h b/Providers/QSDeliciousAPIProvider.h
new file mode 100644
index 0000000..0064264
--- /dev/null
+++ b/Providers/QSDeliciousAPIProvider.h
@@ -0,0 +1,25 @@
+//
+// QSDeliciousAPIProvider.h
+// QSDeliciousPlugIn
+//
+// Base provider for Pinboard v1 API (XML-based with Basic Auth)
+// Used by Delicious, Magnolia, and Pinboard
+//
+
+#import <Foundation/Foundation.h>
+#import "QSBookmarkProvider.h"
+
+@interface QSDeliciousAPIProvider : NSObject <QSBookmarkProvider, NSXMLParserDelegate>
+
+@property (nonatomic, strong) NSMutableArray *posts;
+@property (nonatomic, assign) SocialSite site;
+
+- (instancetype)initWithSite:(SocialSite)site;
+
+// Subclasses can override these methods
+- (NSString *)apiURLForSite:(SocialSite)site;
+- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host;
+- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username;
+- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username;
+
+@end
diff --git a/Providers/QSDeliciousAPIProvider.m b/Providers/QSDeliciousAPIProvider.m
new file mode 100644
index 0000000..ab09f88
--- /dev/null
+++ b/Providers/QSDeliciousAPIProvider.m
@@ -0,0 +1,193 @@
+//
+// QSDeliciousAPIProvider.m
+// QSDeliciousPlugIn
+//
+
+#import "QSDeliciousAPIProvider.h"
+#import "SocialSite.h"
+#import <QSCore/QSCore.h>
+
+@implementation QSDeliciousAPIProvider
+
+- (instancetype)initWithSite:(SocialSite)site {
+ self = [super init];
+ if (self) {
+ _site = site;
+ _posts = [NSMutableArray array];
+ }
+ return self;
+}
+
+- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host {
+ return (site == self.site) &&
+ username.length > 0 &&
+ password.length > 0 &&
+ (site != SocialSiteLinkding); // This provider doesn't handle Linkding
+}
+
+- (SocialSite)supportedSite {
+ return self.site;
+}
+
+- (NSString *)providerName {
+ return [SocialSiteHelper displayNameForSite:self.site];
+}
+
+- (NSString *)apiURLForSite:(SocialSite)site {
+ switch (site) {
+ case SocialSiteDelicious:
+ return @"api.del.icio.us/v1";
+ case SocialSiteMagnolia:
+ return @"ma.gnolia.com/api/mirrord/v1";
+ case SocialSitePinboard:
+ return @"api.pinboard.in/v1";
+ default:
+ return nil;
+ }
+}
+
+- (NSURL *)requestURLForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host {
+ NSString *apiURL = [self apiURLForSite:site];
+ if (!apiURL) return nil;
+
+ NSString *urlString = [NSString stringWithFormat:@"https://%@:%@@%@/posts/all?", username, password, apiURL];
+ return [NSURL URLWithString:urlString];
+}
+
+- (NSData *)cachedBookmarkDataForSite:(SocialSite)site username:(NSString *)username {
+ NSString *siteURL = [SocialSiteHelper siteURLForSite:site];
+ NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]];
+ return [NSData dataWithContentsOfFile:cachePath];
+}
+
+- (void)cacheBookmarkData:(NSData *)data forSite:(SocialSite)site username:(NSString *)username {
+ NSString *siteURL = [SocialSiteHelper siteURLForSite:site];
+ NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/%@/", siteURL], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@.xml", username]];
+ [data writeToFile:cachePath atomically:NO];
+}
+
+- (NSString *)tagURLType {
+ return [NSString stringWithFormat:@"tag.%@", [SocialSiteHelper reversedSiteURLForSite:self.site]];
+}
+
+- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags {
+
+ // Try cached data first
+ NSData *data = [self cachedBookmarkDataForSite:site username:username];
+
+ // If no cached data, fetch from API
+ if (![data length]) {
+ NSURL *requestURL = [self requestURLForSite:site username:username password:password host:host];
+ if (!requestURL) return @[];
+
+ NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL
+ cachePolicy:NSURLRequestUseProtocolCachePolicy
+ timeoutInterval:60.0];
+ [theRequest setHTTPMethod:@"POST"];
+ [theRequest setValue:@"text/xml" forHTTPHeaderField:@"Content-type"];
+ [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"];
+
+ NSError *error;
+ data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error];
+
+ if (error) {
+ NSLog(@"Error fetching bookmarks: %@", error.localizedDescription);
+ return @[];
+ }
+
+ // Cache the data
+ [self cacheBookmarkData:data forSite:site username:username];
+ }
+
+ // Parse XML data
+ NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data];
+ [postParser setDelegate:self];
+
+ self.posts = [NSMutableArray arrayWithCapacity:1];
+ [postParser parse];
+
+ NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1];
+ NSMutableSet *tagSet = [NSMutableSet set];
+
+ // Create bookmark objects
+ for (NSDictionary *post in self.posts) {
+ QSObject *newObject = [self objectForPost:post];
+ if (newObject) {
+ [objects addObject:newObject];
+
+ // Collect tags if requested
+ if (includeTags) {
+ NSString *tagString = [post objectForKey:@"tag"];
+ if (tagString.length > 0) {
+ [tagSet addObjectsFromArray:[tagString componentsSeparatedByString:@" "]];
+ }
+ }
+ }
+ }
+
+ // Create tag objects if requested
+ if (includeTags) {
+ for (NSString *tag in tagSet) {
+ if (tag.length > 0) {
+ QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[%@ tag]:%@", [self providerName], tag]];
+ [tagObject setObject:tag forType:[self tagURLType]];
+ [tagObject setObject:@(site) forMeta:@"source.site"];
+ [tagObject setObject:username forMeta:@"source.username"];
+ [tagObject setObject:host forMeta:@"source.host"];
+ [tagObject setObject:identifier forMeta:@"source.identifier"];
+ [tagObject setName:tag];
+ [tagObject setPrimaryType:[self tagURLType]];
+ [objects addObject:tagObject];
+ }
+ }
+ }
+
+ return objects;
+}
+
+- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host {
+ NSData *data = [self cachedBookmarkDataForSite:site username:username];
+ if (!data) return @[];
+
+ NSXMLParser *postParser = [[NSXMLParser alloc] initWithData:data];
+ [postParser setDelegate:self];
+ self.posts = [NSMutableArray arrayWithCapacity:1];
+ [postParser parse];
+
+ NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1];
+
+ for (NSDictionary *post in self.posts) {
+ NSString *postTags = [post objectForKey:@"tag"];
+ if ([postTags rangeOfString:tag].location != NSNotFound) {
+ QSObject *newObject = [self objectForPost:post];
+ if (newObject) {
+ [objects addObject:newObject];
+ }
+ }
+ }
+
+ return objects;
+}
+
+- (QSObject *)objectForPost:(NSDictionary *)post {
+ QSObject *newObject = [QSObject makeObjectWithIdentifier:[post objectForKey:@"hash"]];
+ [newObject setObject:[post objectForKey:@"href"] forType:QSURLType];
+ [newObject setName:[post objectForKey:@"description"]];
+ [newObject setDetails:[post objectForKey:@"extended"]];
+ [newObject setPrimaryType:QSURLType];
+ return newObject;
+}
+
+#pragma mark - NSXMLParserDelegate
+
+- (void)parser:(NSXMLParser*)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict {
+ if ([elementName isEqualToString:@"post"] && attributeDict) {
+ [self.posts addObject:attributeDict];
+ }
+}
+
+- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName {
+ // Implementation if needed
+}
+
+@end
diff --git a/Providers/QSLinkdingProvider.h b/Providers/QSLinkdingProvider.h
new file mode 100644
index 0000000..28807e8
--- /dev/null
+++ b/Providers/QSLinkdingProvider.h
@@ -0,0 +1,16 @@
+//
+// QSLinkdingProvider.h
+// QSDeliciousPlugIn
+//
+// Provider for Linkding API (JSON-based with API key)
+//
+
+#import <Foundation/Foundation.h>
+#import "QSBookmarkProvider.h"
+
+@interface QSLinkdingProvider : NSObject <QSBookmarkProvider>
+
+- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username;
+- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username;
+
+@end \ No newline at end of file
diff --git a/Providers/QSLinkdingProvider.m b/Providers/QSLinkdingProvider.m
new file mode 100644
index 0000000..094f318
--- /dev/null
+++ b/Providers/QSLinkdingProvider.m
@@ -0,0 +1,192 @@
+//
+// QSLinkdingProvider.m
+// QSDeliciousPlugIn
+//
+
+#import "QSLinkdingProvider.h"
+#import "SocialSite.h"
+#import <QSCore/QSCore.h>
+
+@implementation QSLinkdingProvider
+
+- (BOOL)canHandleSite:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host {
+ return (site == SocialSiteLinkding) &&
+ username.length > 0 &&
+ password.length > 0 && // password is API token for Linkding
+ host.length > 0;
+}
+
+- (SocialSite)supportedSite {
+ return SocialSiteLinkding;
+}
+
+- (NSString *)providerName {
+ return @"Linkding";
+}
+
+- (NSString *)tagURLType {
+ return @"tag.linkding";
+}
+
+- (NSData *)cachedBookmarkDataForHost:(NSString *)host username:(NSString *)username {
+ // Create a safe filename from host
+ NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"];
+ NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], NO) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]];
+ return [NSData dataWithContentsOfFile:cachePath];
+}
+
+- (void)cacheBookmarkData:(NSData *)data forHost:(NSString *)host username:(NSString *)username {
+ // Create a safe filename from host
+ NSString *safeHost = [[host componentsSeparatedByCharactersInSet:[[NSCharacterSet alphanumericCharacterSet] invertedSet]] componentsJoinedByString:@"-"];
+ NSString *cachePath = [QSApplicationSupportSubPath([NSString stringWithFormat:@"Caches/linkding/"], YES) stringByAppendingPathComponent:[NSString stringWithFormat:@"%@-%@.json", safeHost, username]];
+ [data writeToFile:cachePath atomically:NO];
+}
+
+- (NSArray *)fetchBookmarksForSite:(SocialSite)site username:(NSString *)username password:(NSString *)password identifier:(NSString *)identifier host:(NSString *)host includeTags:(BOOL)includeTags {
+
+ if (![self canHandleSite:site username:username password:password host:host]) {
+ return @[];
+ }
+
+ // Try cached data first
+ NSData *data = [self cachedBookmarkDataForHost:host username:username];
+
+ // If no cached data, fetch from API
+ if (![data length]) {
+ // Construct Linkding API URL
+ NSString *baseURL = host;
+ if (![baseURL hasPrefix:@"http://"] && ![baseURL hasPrefix:@"https://"]) {
+ baseURL = [NSString stringWithFormat:@"https://%@", baseURL];
+ }
+ if ([baseURL hasSuffix:@"/"]) {
+ baseURL = [baseURL substringToIndex:[baseURL length] - 1];
+ }
+
+ NSString *urlString = [NSString stringWithFormat:@"%@/api/bookmarks/", baseURL];
+ NSURL *requestURL = [NSURL URLWithString:urlString];
+
+ if (!requestURL) {
+ NSLog(@"Invalid Linkding host URL: %@", host);
+ return @[];
+ }
+
+ NSMutableURLRequest *theRequest = [NSMutableURLRequest requestWithURL:requestURL
+ cachePolicy:NSURLRequestUseProtocolCachePolicy
+ timeoutInterval:60.0];
+ [theRequest setHTTPMethod:@"GET"];
+ [theRequest setValue:@"application/json" forHTTPHeaderField:@"Accept"];
+ [theRequest setValue:[NSString stringWithFormat:@"Token %@", password] forHTTPHeaderField:@"Authorization"];
+ [theRequest setValue:@"Quicksilver (Blacktree,MacOSX)" forHTTPHeaderField:@"User-Agent"];
+
+ NSError *error = nil;
+ data = [NSURLConnection sendSynchronousRequest:theRequest returningResponse:nil error:&error];
+
+ if (error) {
+ NSLog(@"Error fetching Linkding bookmarks: %@", error.localizedDescription);
+ return @[];
+ }
+
+ // Cache the data
+ [self cacheBookmarkData:data forHost:host username:username];
+ }
+
+ // Parse JSON data
+ NSError *jsonError = nil;
+ NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
+
+ if (jsonError) {
+ NSLog(@"Error parsing Linkding JSON: %@", jsonError.localizedDescription);
+ return @[];
+ }
+
+ NSArray *results = [jsonResponse objectForKey:@"results"];
+ if (!results || ![results isKindOfClass:[NSArray class]]) {
+ NSLog(@"Invalid Linkding response format");
+ return @[];
+ }
+
+ NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1];
+ NSMutableSet *tagSet = [NSMutableSet set];
+
+ // Create bookmark objects
+ for (NSDictionary *bookmark in results) {
+ QSObject *newObject = [self objectForLinkdingBookmark:bookmark];
+ if (newObject) {
+ [objects addObject:newObject];
+
+ // Collect tags if requested
+ if (includeTags) {
+ NSArray *tags = [bookmark objectForKey:@"tag_names"];
+ if (tags && [tags isKindOfClass:[NSArray class]]) {
+ [tagSet addObjectsFromArray:tags];
+ }
+ }
+ }
+ }
+
+ // Create tag objects if requested
+ if (includeTags) {
+ for (NSString *tag in tagSet) {
+ if (tag.length > 0) {
+ QSObject *tagObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"[Linkding tag]:%@", tag]];
+ [tagObject setObject:tag forType:[self tagURLType]];
+ [tagObject setObject:@(site) forMeta:@"source.site"];
+ [tagObject setObject:username forMeta:@"source.username"];
+ [tagObject setObject:host forMeta:@"source.host"];
+ // We need the identifier to be able to fetch the keychain password
+ [tagObject setObject:identifier forMeta:@"source.identifier"];
+ [tagObject setName:tag];
+ [tagObject setPrimaryType:[self tagURLType]];
+ [objects addObject:tagObject];
+ }
+ }
+ }
+
+ return objects;
+}
+
+- (NSArray *)fetchBookmarksForTag:(NSString *)tag site:(SocialSite)site username:(NSString *)username password:(NSString *)password host:(NSString *)host {
+ NSData *data = [self cachedBookmarkDataForHost:host username:username];
+ if (!data) return @[];
+
+ NSError *jsonError;
+ NSDictionary *jsonResponse = [NSJSONSerialization JSONObjectWithData:data options:0 error:&jsonError];
+
+ if (jsonError) return @[];
+
+ NSArray *results = [jsonResponse objectForKey:@"results"];
+ if (!results || ![results isKindOfClass:[NSArray class]]) return @[];
+
+ NSMutableArray *objects = [NSMutableArray arrayWithCapacity:1];
+
+ for (NSDictionary *bookmark in results) {
+ NSArray *tags = [bookmark objectForKey:@"tag_names"];
+ if (tags && [tags isKindOfClass:[NSArray class]] && [tags containsObject:tag]) {
+ QSObject *newObject = [self objectForLinkdingBookmark:bookmark];
+ if (newObject) {
+ [objects addObject:newObject];
+ }
+ }
+ }
+
+ return objects;
+}
+
+- (QSObject *)objectForLinkdingBookmark:(NSDictionary *)bookmark {
+ NSNumber *bookmarkId = [bookmark objectForKey:@"id"];
+ NSString *url = [bookmark objectForKey:@"url"];
+ NSString *title = [bookmark objectForKey:@"title"];
+ NSString *description = [bookmark objectForKey:@"description"];
+
+ if (!bookmarkId || !url) return nil;
+
+ QSObject *newObject = [QSObject makeObjectWithIdentifier:[NSString stringWithFormat:@"linkding-%@", bookmarkId]];
+ [newObject setObject:url forType:QSURLType];
+ [newObject setName:title.length > 0 ? title : url];
+ [newObject setDetails:description.length > 0 ? description : @""];
+ [newObject setPrimaryType:QSURLType];
+
+ return newObject;
+}
+
+@end