Last active
January 26, 2023 19:49
-
-
Save nickludlam/ce7f72ce62267e0a6ba9ed70582a27fe to your computer and use it in GitHub Desktop.
Sync Mastodon bookmarks with an instance of the bookmarking website Linkding at https://github.com/sissbruecker/linkding
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
"""Sync Mastodon bookmarks to Linkding | |
This script will automatically fetch a user's bookmarks from a | |
Mastodon instance. If the bookmark is not yet present in the | |
Linkding bookmark site, it's created as an unread, along with | |
any other things like custom tags. | |
If the bookmarked Mastodon toot only contains one link, excluding | |
any others like user mentions and hashtags, then that link is | |
used for the bookmark URL, instead of the toot itself. | |
""" | |
import html.parser | |
import itertools | |
import json | |
import os | |
import requests | |
import sys | |
MASTODON_BOOKMARKS_URL = 'https://amok.recoil.org/api/v1/bookmarks' | |
LINKDING_SITE="http://bookmarks.local/" | |
# Parse the HTML contained in the response from Mastodon | |
class LinkParser(html.parser.HTMLParser): | |
# From looking at the API, most extraneous tags will be covered by excluding the following classes | |
# when we parse the 'content' field returned from Mastodon's API | |
excluded_anchor_classes = ['mention', 'hashtag'] | |
def reset(self): | |
super().reset() | |
self.links = iter([]) | |
def handle_starttag(self, tag, attrs): | |
if tag == 'a': | |
anchor_classes = [item[1] for item in attrs if item[0] == 'class'] | |
for excluded_anchor_class in self.excluded_anchor_classes: | |
for anchor_class in anchor_classes: | |
if excluded_anchor_class in anchor_class: | |
# We don't want any links that have a class corresponding to excluded_anchor_classes | |
return | |
# Keep the rest of the links | |
for (name, value) in attrs: | |
if name == 'href': | |
self.links = itertools.chain(self.links, [value]) | |
# Check if we have an existing bookmark for this URL | |
def bookmark_exists(linkding_api_token, url): | |
headers = { 'Authorization': f'Token {linkding_api_token}' } | |
params = { 'q' : url } | |
r = requests.get(LINKDING_SITE + 'api/bookmarks/', params=params, headers=headers) | |
if r.status_code != 200: | |
print("Failed to check link existence on the bookmarks server") | |
existing_bookmarks = json.loads(r.content) | |
count = existing_bookmarks['count'] | |
return count != 0 | |
def create_bookmark(linkding_api_token, url, additional_description=None): | |
if bookmark_exists(linkding_api_token, url): | |
return | |
headers = { 'Authorization': f'Token {linkding_api_token}' } | |
params = { 'url' : url } | |
# These might need some alteration to find a suitable workflow | |
if additional_description != None: | |
params['description'] = additional_description | |
params['unread'] = True | |
params['tag_names'] = ['mastodonautobookmark'] | |
r = requests.post(LINKDING_SITE + 'api/bookmarks/', data=params, headers=headers) | |
if r.status_code != 201: | |
raise Exception(f"Failed to create a link on the bookmarks server {r.status_code}: {r.content}") | |
def process_bookmark_toot(linkding_api_token, mastodon_bookmark_url, mastodon_bookmark_links): | |
# If there's only one URL contained in the toot, we 'unwrap' it and bookmark that URL directly, | |
# instead of the Mastodon toot itself. Otherwise we bookmark the toot | |
if len(mastodon_bookmark_links) == 1: | |
create_bookmark(linkding_api_token, mastodon_bookmark_links[0], f"Bookmarked from {mastodon_bookmark_url}") | |
else: | |
create_bookmark(linkding_api_token, mastodon_bookmark_url) | |
def fetch_mastodon_bookmarks(mastodon_api_token): | |
headers = {'Authorization': f'Bearer {mastodon_api_token}' } | |
r = requests.get(MASTODON_BOOKMARKS_URL, headers=headers) | |
if r.status_code != 200: | |
print("Failed to contact mastodon server") | |
bookmarks = json.loads(r.content) | |
response = [] | |
parser = LinkParser() | |
for bookmark in bookmarks: | |
bookmark_url = bookmark['url'] | |
#print(f"Processing bookmark toot: {bookmark_url}") | |
content = bookmark['content'] | |
parser.feed(content) | |
response.append((bookmark_url, list(parser.links))) | |
return response | |
if __name__ == '__main__': | |
mastodon_api_token = os.getenv("MASTODON_API_TOKEN") | |
if mastodon_api_token == None: | |
print("Please ensure you specify MASTODON_API_TOKEN in the environment") | |
sys.exit(1) | |
linkding_api_token = os.getenv("LINKDING_API_TOKEN") | |
if linkding_api_token == None: | |
print("Please ensure you specify LINKDING_API_TOKEN in the environment") | |
sys.exit(1) | |
bookmarks_and_links = fetch_mastodon_bookmarks(mastodon_api_token) | |
for (mastodon_bookmark_url, mastodon_bookmark_links) in bookmarks_and_links: | |
process_bookmark_toot(linkding_api_token, mastodon_bookmark_url, mastodon_bookmark_links) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment