Skip to content

Instantly share code, notes, and snippets.

@paultopia
Last active August 21, 2023 02:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save paultopia/b7bb52425a2c7a6436871dc3cbac67fc to your computer and use it in GitHub Desktop.
Save paultopia/b7bb52425a2c7a6436871dc3cbac67fc to your computer and use it in GitHub Desktop.
post bluesky tweet thread
# Quick and dirty script to post a bluesky thread via API
# because I hate having to manually break up long text at character count limits
# largely adapted/swiped from the more full-featured library at https://github.com/ianklatzco/atprototools/tree/master
import requests
import datetime
import textwrap
import appex # this is a pythonista (ipad/ios python app) library for share sheet functionality, omit on real computer
import sys
import keychain # pythonista library, for a real computer use envirnoment variables instead
import re
# POSTING FUNCTIONALITY
# Authentication functionality
# ALSO NOTE keychain doesn't work for share extension, which can't access central pythonista keychain
# currently just passing myself in in-app settings for app extension version
username = "YOUR@EMAILHERE.COM"
def fetch_login_info(username):
if appex.is_running_extension():
password = sys.argv[1]
else:
password = keychain.get_password("bluesky", username)
return password
# Session information
server = "https://bsky.social"
REPO = "" # I think this is an internal account identifier?
AUTH_TOKEN = ""
# should really just do all of this in a class like a proper python guy but fuckit
def login(username):
password = fetch_login_info(username)
endpoint = server + "/xrpc/com.atproto.server.createSession"
data = {"identifier": username, "password": password}
response = requests.post(endpoint, json=data)
return response.json().get("did"), response.json().get('accessJwt')
def get_first_link_location(content):
regex = r'https[^\s]*'
match = re.search(regex, content)
return match
def post(content, reply_to=None):
endpoint = server + "/xrpc/com.atproto.repo.createRecord"
headers = {"Authorization": "Bearer " + AUTH_TOKEN}
timestamp = datetime.datetime.now(datetime.timezone.utc).isoformat().replace('+00:00', 'Z')
data = {
"collection": "app.bsky.feed.post",
"$type": "app.bsky.feed.post",
"repo": REPO,
"record": {
"$type": "app.bsky.feed.post",
"createdAt": timestamp,
"text": content,
}
}
if reply_to:
data['record']['reply'] = reply_to
link = get_first_link_location(content)
if link:
byteStart, byteEnd = link.span()
uri = link.group(0)
data["record"]["facets"] = [{"index":
{"byteStart": byteStart, "byteEnd": byteEnd},
"features":[{"uri":uri, "$type":"app.bsky.richtext.facet#link"}]}]
resp = requests.post(endpoint, json=data, headers=headers)
return resp
# TEXT BREAKING UP FUNCTIONALITY
def breakup(content):
start = "THREAD: "
postend = " (cont.)"
maxlength = 300
return textwrap.wrap(content, width = maxlength, initial_indent=start, replace_whitespace=False, placeholder=postend)
# for some reason the placeholder argument doesn't work, at least on the pythonista app. annoying, but not a big deal
# will have to test on a real computer with a more current version of python later.
def post_thread(content):
if len(content) <= 300:
post(content)
else:
tweets = breakup(content)
first = tweets.pop(0)
firstpost = post(first)
first_uri = firstpost.json().get('uri')
first_cid = firstpost.json().get('cid')
current_uri = first_uri
current_cit = first_cid
for tweet in tweets:
reply_to = {"root": {"cid": first_cid, "uri": first_uri}, "parent": {"cid": current_cit, "uri": current_uri}}
response = post(tweet, reply_to = reply_to)
current_uri = response.json().get("uri")
current_cid = response.json().get("cid")
if __name__ == "__main__":
if appex.is_running_extension():
content = appex.get_text()
else:
content = """Hi! I'm content! And this is a test to see if I work for posting more than 300 characters in a thread on bluesky.
Don't worry, I'll be deleting this silly thread after all is said and done, no worries to be had. Also, it's surprisingly difficult to come up with more than 300 junk characters to type in order to text. Hi, I'm words, made of characters, made up of integers in UTF-8 format. Isn't that amazing? I also want to test multiline content here, so here we are, testing multiline content.
Yay multiline content! And now I'm going to put some loren ipsum text in here in order to make it possible to have an even bigger set of texts and see how this does with like 4 or more outputs. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque semper mauris vel sodales luctus. Aliquam facilisis pretium leo sollicitudin volutpat. Etiam tellus velit, ornare ut sem ut, tempor scelerisque augue. Aenean posuere ut felis sagittis tincidunt. Integer id libero eget ex placerat lobortis. Morbi neque leo, maximus nec condimentum vitae, aliquet sit amet mi. Maecenas porttitor bibendum interdum. Phasellus in egestas risus, ut ultrices arcu. Mauris dapibus enim at efficitur scelerisque. Vivamus volutpat iaculis libero. Aenean hendrerit elementum sollicitudin. Praesent condimentum diam sagittis neque congue, eu auctor erat semper. Cras sed magna sagittis, euismod elit vel, maximus dui. Quisque sed."""
REPO, AUTH_TOKEN = login(username)
post_thread(content)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment