Skip to content

Instantly share code, notes, and snippets.

@nickludlam
Last active January 26, 2023 19:49
Show Gist options
  • Save nickludlam/ce7f72ce62267e0a6ba9ed70582a27fe to your computer and use it in GitHub Desktop.
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
"""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