epicyon/posts.py

3424 lines
129 KiB
Python
Raw Normal View History

2020-04-04 10:05:27 +00:00
__filename__ = "posts.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.1.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
2019-06-28 18:55:29 +00:00
import json
import html
2019-06-29 10:08:59 +00:00
import datetime
2019-06-30 15:03:26 +00:00
import os
import shutil
import sys
2019-07-01 11:48:54 +00:00
import time
2020-06-23 21:39:19 +00:00
from socket import error as SocketError
2019-10-10 13:48:05 +00:00
from time import gmtime, strftime
from collections import OrderedDict
2019-06-30 16:36:58 +00:00
from threads import threadWithTrace
2019-06-30 15:03:26 +00:00
from cache import storePersonInCache
from cache import getPersonFromCache
2019-08-20 11:51:29 +00:00
from cache import expirePersonCache
2019-06-29 10:08:59 +00:00
from pprint import pprint
2019-07-03 10:33:55 +00:00
from session import createSession
2019-06-28 18:55:29 +00:00
from session import getJson
2020-04-04 10:05:27 +00:00
from session import postJson
from session import postJsonString
2019-07-16 14:23:06 +00:00
from session import postImage
2019-06-30 22:56:37 +00:00
from webfinger import webfingerHandle
2019-07-01 09:31:02 +00:00
from httpsig import createSignedHeader
from utils import siteIsActive
2019-12-01 13:57:43 +00:00
from utils import removePostFromCache
2019-12-01 13:51:44 +00:00
from utils import getCachedPostFilename
2019-07-02 09:25:29 +00:00
from utils import getStatusNumber
2019-07-04 16:24:23 +00:00
from utils import createPersonDir
2019-07-02 10:39:55 +00:00
from utils import urlPermitted
2019-07-09 14:20:23 +00:00
from utils import getNicknameFromActor
from utils import getDomainFromActor
2019-07-14 16:37:01 +00:00
from utils import deletePost
2019-07-27 22:48:34 +00:00
from utils import validNickname
from utils import locatePost
2019-10-22 11:55:06 +00:00
from utils import loadJson
from utils import saveJson
from capabilities import getOcapFilename
2019-07-09 14:20:23 +00:00
from capabilities import capabilitiesUpdate
2019-08-30 18:45:14 +00:00
from media import attachMedia
2020-01-15 22:31:04 +00:00
from media import replaceYouTube
2020-05-17 09:27:51 +00:00
from content import removeLongWords
2019-08-09 09:09:21 +00:00
from content import addHtmlTags
2019-09-29 17:42:51 +00:00
from content import replaceEmojiFromTags
from content import removeTextFormatting
2019-07-16 10:19:04 +00:00
from auth import createBasicAuthHeader
2019-08-11 11:25:27 +00:00
from config import getConfigParam
2019-09-28 16:31:03 +00:00
from blocking import isBlocked
2020-02-05 14:57:10 +00:00
from filters import isFiltered
2020-05-03 12:52:13 +00:00
from git import convertPostToPatch
2020-06-15 13:08:19 +00:00
from jsonldsig import jsonldSign
2020-04-04 10:05:27 +00:00
# try:
# from BeautifulSoup import BeautifulSoup
# except ImportError:
# from bs4 import BeautifulSoup
2019-06-28 18:55:29 +00:00
2020-04-04 10:05:27 +00:00
def isModerator(baseDir: str, nickname: str) -> bool:
2019-08-12 13:22:17 +00:00
"""Returns true if the given nickname is a moderator
"""
2020-04-04 10:05:27 +00:00
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-12 13:22:17 +00:00
if not os.path.isfile(moderatorsFile):
2020-04-04 10:05:27 +00:00
if getConfigParam(baseDir, 'admin') == nickname:
2019-08-12 13:22:17 +00:00
return True
return False
with open(moderatorsFile, "r") as f:
2020-04-04 10:05:27 +00:00
lines = f.readlines()
if len(lines) == 0:
if getConfigParam(baseDir, 'admin') == nickname:
2019-08-12 13:22:17 +00:00
return True
for moderator in lines:
2020-05-22 11:32:38 +00:00
moderator = moderator.strip('\n').strip('\r')
2020-04-04 10:05:27 +00:00
if moderator == nickname:
2019-08-12 13:22:17 +00:00
return True
return False
2020-04-04 10:05:27 +00:00
def noOfFollowersOnDomain(baseDir: str, handle: str,
2019-07-06 17:00:22 +00:00
domain: str, followFile='followers.txt') -> int:
2019-07-05 14:39:24 +00:00
"""Returns the number of followers of the given handle from the given domain
"""
2020-04-04 10:05:27 +00:00
filename = baseDir + '/accounts/' + handle + '/' + followFile
2019-07-05 14:39:24 +00:00
if not os.path.isfile(filename):
return 0
2020-04-04 10:05:27 +00:00
ctr = 0
2019-07-05 14:39:24 +00:00
with open(filename, "r") as followersFilename:
for followerHandle in followersFilename:
if '@' in followerHandle:
2020-05-22 11:32:38 +00:00
followerDomain = followerHandle.split('@')[1]
followerDomain = followerDomain.replace('\n', '')
followerDomain = followerDomain.replace('\r', '')
2020-04-04 10:05:27 +00:00
if domain == followerDomain:
ctr += 1
2019-07-05 14:39:24 +00:00
return ctr
2020-04-04 10:05:27 +00:00
def getPersonKey(nickname: str, domain: str, baseDir: str, keyType='public',
2019-07-06 17:00:22 +00:00
debug=False):
2019-06-30 15:03:26 +00:00
"""Returns the public or private key of a person
"""
2020-04-04 10:05:27 +00:00
handle = nickname + '@' + domain
keyFilename = baseDir + '/keys/' + keyType + '/' + handle.lower() + '.key'
2019-06-30 15:03:26 +00:00
if not os.path.isfile(keyFilename):
2019-07-06 13:49:25 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: private key file not found: ' + keyFilename)
2019-06-30 15:03:26 +00:00
return ''
2020-04-04 10:05:27 +00:00
keyPem = ''
2019-06-30 15:03:26 +00:00
with open(keyFilename, "r") as pemFile:
2020-04-04 10:05:27 +00:00
keyPem = pemFile.read()
if len(keyPem) < 20:
2019-07-06 13:49:25 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: private key was too short: ' + keyPem)
2019-06-30 15:03:26 +00:00
return ''
return keyPem
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
2019-06-28 18:55:29 +00:00
def cleanHtml(rawHtml: str) -> str:
2020-04-04 10:05:27 +00:00
# text=BeautifulSoup(rawHtml, 'html.parser').get_text()
text = rawHtml
2019-06-28 18:55:29 +00:00
return html.unescape(text)
2020-04-04 10:05:27 +00:00
2019-10-21 15:55:30 +00:00
def getUserUrl(wfRequest: {}) -> str:
2019-06-28 18:55:29 +00:00
if wfRequest.get('links'):
for link in wfRequest['links']:
if link.get('type') and link.get('href'):
2019-11-09 21:39:04 +00:00
if link['type'] == 'application/activity+json':
2020-04-04 10:05:27 +00:00
if not ('/users/' in link['href'] or
'/profile/' in link['href'] or
2019-10-21 16:15:12 +00:00
'/channel/' in link['href']):
2020-04-04 10:05:27 +00:00
print('Webfinger activity+json contains ' +
'single user instance actor')
2019-10-21 16:15:12 +00:00
return link['href']
2019-06-28 18:55:29 +00:00
return None
2020-04-04 10:05:27 +00:00
def parseUserFeed(session, feedUrl: str, asHeader: {},
projectVersion: str, httpPrefix: str,
domain: str) -> None:
feedJson = getJson(session, feedUrl, asHeader, None,
projectVersion, httpPrefix, domain)
2019-07-04 17:31:41 +00:00
if not feedJson:
return
2019-06-28 18:55:29 +00:00
2019-06-29 10:08:59 +00:00
if 'orderedItems' in feedJson:
2019-06-29 10:59:16 +00:00
for item in feedJson['orderedItems']:
2019-06-28 18:55:29 +00:00
yield item
2020-04-04 10:05:27 +00:00
nextUrl = None
2019-06-29 10:08:59 +00:00
if 'first' in feedJson:
2020-04-04 10:05:27 +00:00
nextUrl = feedJson['first']
2019-06-29 10:08:59 +00:00
elif 'next' in feedJson:
2020-04-04 10:05:27 +00:00
nextUrl = feedJson['next']
2019-06-28 18:55:29 +00:00
if nextUrl:
2019-09-01 13:13:52 +00:00
if isinstance(nextUrl, str):
2020-04-04 10:05:27 +00:00
userFeed = parseUserFeed(session, nextUrl, asHeader,
projectVersion, httpPrefix,
domain)
2019-09-01 13:13:52 +00:00
for item in userFeed:
yield item
elif isinstance(nextUrl, dict):
2020-04-04 10:05:27 +00:00
userFeed = nextUrl
2019-09-01 13:13:52 +00:00
if userFeed.get('orderedItems'):
for item in userFeed['orderedItems']:
2020-03-22 21:16:02 +00:00
yield item
2020-04-04 10:05:27 +00:00
def getPersonBox(baseDir: str, session, wfRequest: {},
personCache: {},
projectVersion: str, httpPrefix: str,
nickname: str, domain: str,
boxName='inbox') -> (str, str, str, str, str, str, str, str):
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/activity+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2019-10-17 15:55:05 +00:00
if not wfRequest.get('errors'):
2020-04-04 10:05:27 +00:00
personUrl = getUserUrl(wfRequest)
2019-10-17 15:55:05 +00:00
else:
2020-04-04 10:05:27 +00:00
if nickname == 'dev':
2019-10-21 16:03:44 +00:00
# try single user instance
print('getPersonBox: Trying single user instance with ld+json')
2020-04-04 10:05:27 +00:00
personUrl = httpPrefix + '://' + domain
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2019-10-21 16:03:44 +00:00
else:
2020-04-04 10:05:27 +00:00
personUrl = httpPrefix + '://' + domain + '/users/' + nickname
2019-06-30 10:14:02 +00:00
if not personUrl:
2020-04-04 10:05:27 +00:00
return None, None, None, None, None, None, None, None
personJson = getPersonFromCache(baseDir, personUrl, personCache)
2019-06-30 11:34:19 +00:00
if not personJson:
2019-10-17 22:26:47 +00:00
if '/channel/' in personUrl:
2020-04-04 10:05:27 +00:00
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2020-04-04 10:05:27 +00:00
personJson = getJson(session, personUrl, asHeader, None,
projectVersion, httpPrefix, domain)
2019-07-04 17:31:41 +00:00
if not personJson:
2020-04-04 10:05:27 +00:00
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2020-04-04 10:05:27 +00:00
personJson = getJson(session, personUrl, asHeader, None,
projectVersion, httpPrefix, domain)
2019-10-21 16:20:33 +00:00
if not personJson:
2019-10-21 16:21:16 +00:00
print('Unable to get actor')
2020-04-04 10:05:27 +00:00
return None, None, None, None, None, None, None, None
boxJson = None
2019-06-30 10:14:02 +00:00
if not personJson.get(boxName):
2019-07-05 13:38:29 +00:00
if personJson.get('endpoints'):
if personJson['endpoints'].get(boxName):
2020-04-04 10:05:27 +00:00
boxJson = personJson['endpoints'][boxName]
2019-07-05 13:38:29 +00:00
else:
2020-04-04 10:05:27 +00:00
boxJson = personJson[boxName]
2019-07-05 13:38:29 +00:00
if not boxJson:
2020-04-04 10:05:27 +00:00
return None, None, None, None, None, None, None, None
2019-07-05 13:38:29 +00:00
2020-04-04 10:05:27 +00:00
personId = None
2019-06-30 10:14:02 +00:00
if personJson.get('id'):
2020-04-04 10:05:27 +00:00
personId = personJson['id']
pubKeyId = None
pubKey = None
2019-06-30 10:14:02 +00:00
if personJson.get('publicKey'):
2019-07-01 10:25:03 +00:00
if personJson['publicKey'].get('id'):
2020-04-04 10:05:27 +00:00
pubKeyId = personJson['publicKey']['id']
2019-06-30 10:14:02 +00:00
if personJson['publicKey'].get('publicKeyPem'):
2020-04-04 10:05:27 +00:00
pubKey = personJson['publicKey']['publicKeyPem']
sharedInbox = None
2019-07-05 13:50:27 +00:00
if personJson.get('sharedInbox'):
2020-04-04 10:05:27 +00:00
sharedInbox = personJson['sharedInbox']
2019-07-05 13:50:27 +00:00
else:
if personJson.get('endpoints'):
if personJson['endpoints'].get('sharedInbox'):
2020-04-04 10:05:27 +00:00
sharedInbox = personJson['endpoints']['sharedInbox']
capabilityAcquisition = None
2019-07-06 17:00:22 +00:00
if personJson.get('capabilityAcquisitionEndpoint'):
2020-04-04 10:05:27 +00:00
capabilityAcquisition = personJson['capabilityAcquisitionEndpoint']
avatarUrl = None
2019-07-22 14:09:21 +00:00
if personJson.get('icon'):
if personJson['icon'].get('url'):
2020-04-04 10:05:27 +00:00
avatarUrl = personJson['icon']['url']
displayName = None
if personJson.get('name'):
2020-04-04 10:05:27 +00:00
displayName = personJson['name']
storePersonInCache(baseDir, personUrl, personJson, personCache)
2019-06-30 10:21:07 +00:00
2020-04-04 10:05:27 +00:00
return boxJson, pubKeyId, pubKey, personId, sharedInbox, \
capabilityAcquisition, avatarUrl, displayName
2019-06-30 10:21:07 +00:00
2019-06-30 10:14:02 +00:00
2020-04-04 10:05:27 +00:00
def getPosts(session, outboxUrl: str, maxPosts: int,
maxMentions: int,
maxEmoji: int, maxAttachments: int,
federationList: [],
personCache: {}, raw: bool,
simple: bool, debug: bool,
projectVersion: str, httpPrefix: str,
domain: str) -> {}:
2019-07-28 11:08:14 +00:00
"""Gets public posts from an outbox
"""
2020-04-04 10:05:27 +00:00
personPosts = {}
2019-07-02 09:25:29 +00:00
if not outboxUrl:
return personPosts
2020-04-04 10:05:27 +00:00
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/activity+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2019-10-17 22:26:47 +00:00
if '/outbox/' in outboxUrl:
2020-04-04 10:05:27 +00:00
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-03-22 20:36:19 +00:00
}
2019-07-03 11:24:38 +00:00
if raw:
2020-04-04 10:05:27 +00:00
result = []
i = 0
userFeed = parseUserFeed(session, outboxUrl, asHeader,
projectVersion, httpPrefix, domain)
2019-09-01 13:13:52 +00:00
for item in userFeed:
2019-07-03 11:24:38 +00:00
result.append(item)
i += 1
if i == maxPosts:
break
pprint(result)
return None
2020-04-04 10:05:27 +00:00
i = 0
userFeed = parseUserFeed(session, outboxUrl, asHeader,
projectVersion, httpPrefix, domain)
2019-09-01 13:13:52 +00:00
for item in userFeed:
2019-07-19 16:56:55 +00:00
if not item.get('id'):
if debug:
print('No id')
continue
2019-06-28 18:55:29 +00:00
if not item.get('type'):
2019-07-19 16:56:55 +00:00
if debug:
print('No type')
2019-06-28 18:55:29 +00:00
continue
if item['type'] != 'Create':
2019-07-19 16:56:55 +00:00
if debug:
print('Not Create type')
2019-06-28 18:55:29 +00:00
continue
if not item.get('object'):
2019-07-19 16:56:55 +00:00
if debug:
print('No object')
2019-06-28 18:55:29 +00:00
continue
2019-07-19 16:56:55 +00:00
if not isinstance(item['object'], dict):
if debug:
print('item object is not a dict')
continue
if not item['object'].get('published'):
if debug:
print('No published attribute')
continue
if not personPosts.get(item['id']):
2019-07-28 11:08:14 +00:00
# check that this is a public post
# #Public should appear in the "to" list
if item['object'].get('to'):
2020-04-04 10:05:27 +00:00
isPublic = False
2019-07-28 11:08:14 +00:00
for recipient in item['object']['to']:
if recipient.endswith('#Public'):
2020-04-04 10:05:27 +00:00
isPublic = True
2019-07-28 11:08:14 +00:00
break
if not isPublic:
continue
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
content = \
item['object']['content'].replace('&apos;', "'")
2019-06-28 18:55:29 +00:00
2020-04-04 10:05:27 +00:00
mentions = []
emoji = {}
2019-06-28 18:55:29 +00:00
if item['object'].get('tag'):
for tagItem in item['object']['tag']:
2020-04-04 10:05:27 +00:00
tagType = tagItem['type'].lower()
if tagType == 'emoji':
2019-06-28 18:55:29 +00:00
if tagItem.get('name') and tagItem.get('icon'):
if tagItem['icon'].get('url'):
# No emoji from non-permitted domains
2020-04-04 10:05:27 +00:00
if urlPermitted(tagItem['icon']['url'],
federationList,
2019-07-06 17:00:22 +00:00
"objects:read"):
2020-04-04 10:05:27 +00:00
emojiName = tagItem['name']
emojiIcon = tagItem['icon']['url']
emoji[emojiName] = emojiIcon
2019-07-19 16:56:55 +00:00
else:
if debug:
2020-04-04 10:05:27 +00:00
print('url not permitted ' +
tagItem['icon']['url'])
if tagType == 'mention':
2019-06-28 18:55:29 +00:00
if tagItem.get('name'):
if tagItem['name'] not in mentions:
mentions.append(tagItem['name'])
2020-04-04 10:05:27 +00:00
if len(mentions) > maxMentions:
2019-07-19 16:56:55 +00:00
if debug:
print('max mentions reached')
2019-06-28 18:55:29 +00:00
continue
2020-04-04 10:05:27 +00:00
if len(emoji) > maxEmoji:
2019-07-19 16:56:55 +00:00
if debug:
print('max emojis reached')
2019-06-28 18:55:29 +00:00
continue
2020-04-04 10:05:27 +00:00
summary = ''
2019-06-28 18:55:29 +00:00
if item['object'].get('summary'):
if item['object']['summary']:
2020-04-04 10:05:27 +00:00
summary = item['object']['summary']
2019-06-28 18:55:29 +00:00
2020-04-04 10:05:27 +00:00
inReplyTo = ''
2019-06-28 18:55:29 +00:00
if item['object'].get('inReplyTo'):
if item['object']['inReplyTo']:
# No replies to non-permitted domains
2020-04-04 10:05:27 +00:00
if not urlPermitted(item['object']['inReplyTo'],
federationList,
2019-07-06 17:00:22 +00:00
"objects:read"):
2019-07-19 16:56:55 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('url not permitted ' +
item['object']['inReplyTo'])
2019-06-28 18:55:29 +00:00
continue
2020-04-04 10:05:27 +00:00
inReplyTo = item['object']['inReplyTo']
2019-06-28 18:55:29 +00:00
2020-04-04 10:05:27 +00:00
conversation = ''
2019-06-28 18:55:29 +00:00
if item['object'].get('conversation'):
if item['object']['conversation']:
# no conversations originated in non-permitted domains
2020-04-04 10:05:27 +00:00
if urlPermitted(item['object']['conversation'],
federationList, "objects:read"):
conversation = item['object']['conversation']
2019-06-28 18:55:29 +00:00
2020-04-04 10:05:27 +00:00
attachment = []
2019-06-28 18:55:29 +00:00
if item['object'].get('attachment'):
if item['object']['attachment']:
for attach in item['object']['attachment']:
if attach.get('name') and attach.get('url'):
# no attachments from non-permitted domains
2020-04-04 10:05:27 +00:00
if urlPermitted(attach['url'],
federationList,
2019-07-06 17:00:22 +00:00
"objects:read"):
2020-04-04 10:05:27 +00:00
attachment.append([attach['name'],
attach['url']])
2019-07-19 16:56:55 +00:00
else:
if debug:
2020-04-04 10:05:27 +00:00
print('url not permitted ' +
attach['url'])
2019-06-28 18:55:29 +00:00
2020-04-04 10:05:27 +00:00
sensitive = False
2019-06-28 18:55:29 +00:00
if item['object'].get('sensitive'):
2020-04-04 10:05:27 +00:00
sensitive = item['object']['sensitive']
2019-07-03 11:24:38 +00:00
if simple:
2020-04-04 10:05:27 +00:00
print(cleanHtml(content) + '\n')
2019-07-03 11:24:38 +00:00
else:
2019-07-19 16:56:55 +00:00
pprint(item)
2020-04-04 10:05:27 +00:00
personPosts[item['id']] = {
2019-07-03 11:24:38 +00:00
"sensitive": sensitive,
"inreplyto": inReplyTo,
"summary": summary,
"html": content,
"plaintext": cleanHtml(content),
"attachment": attachment,
"mentions": mentions,
"emoji": emoji,
"conversation": conversation
}
2019-06-28 18:55:29 +00:00
i += 1
if i == maxPosts:
break
2019-07-02 09:25:29 +00:00
return personPosts
2019-06-29 10:08:59 +00:00
2020-04-04 10:05:27 +00:00
def deleteAllPosts(baseDir: str,
nickname: str, domain: str, boxname: str) -> None:
2019-07-04 16:24:23 +00:00
"""Deletes all posts for a person from inbox or outbox
2019-06-29 11:47:33 +00:00
"""
2020-04-04 10:05:27 +00:00
if boxname != 'inbox' and boxname != 'outbox' and boxname != 'tlblogs':
2019-07-04 16:24:23 +00:00
return
2020-04-04 10:05:27 +00:00
boxDir = createPersonDir(nickname, domain, baseDir, boxname)
2019-09-27 12:09:04 +00:00
for deleteFilename in os.scandir(boxDir):
2020-04-04 10:05:27 +00:00
deleteFilename = deleteFilename.name
filePath = os.path.join(boxDir, deleteFilename)
2019-06-29 11:47:33 +00:00
try:
if os.path.isfile(filePath):
os.unlink(filePath)
2020-04-04 10:05:27 +00:00
elif os.path.isdir(filePath):
shutil.rmtree(filePath)
2019-06-29 11:47:33 +00:00
except Exception as e:
print(e)
2020-04-04 10:05:27 +00:00
def savePostToBox(baseDir: str, httpPrefix: str, postId: str,
nickname: str, domain: str, postJsonObject: {},
boxname: str) -> str:
2019-07-04 16:24:23 +00:00
"""Saves the give json to the give box
Returns the filename
"""
2020-04-04 10:05:27 +00:00
if boxname != 'inbox' and boxname != 'outbox' and \
boxname != 'tlblogs' and boxname != 'scheduled':
return None
2020-04-04 10:05:27 +00:00
originalDomain = domain
if ':' in domain:
2020-04-04 10:05:27 +00:00
domain = domain.split(':')[0]
2019-07-04 16:24:23 +00:00
2019-07-03 22:59:56 +00:00
if not postId:
2020-04-04 10:05:27 +00:00
statusNumber, published = getStatusNumber()
postId = \
httpPrefix + '://' + originalDomain + '/users/' + nickname + \
'/statuses/' + statusNumber
postJsonObject['id'] = postId + '/activity'
2019-07-14 16:57:06 +00:00
if postJsonObject.get('object'):
2019-07-16 19:07:45 +00:00
if isinstance(postJsonObject['object'], dict):
2020-04-04 10:05:27 +00:00
postJsonObject['object']['id'] = postId
postJsonObject['object']['atomUri'] = postId
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
boxDir = createPersonDir(nickname, domain, baseDir, boxname)
filename = boxDir + '/' + postId.replace('/', '#') + '.json'
saveJson(postJsonObject, filename)
return filename
2020-04-04 10:05:27 +00:00
def updateHashtagsIndex(baseDir: str, tag: {}, newPostId: str) -> None:
2019-08-09 11:12:08 +00:00
"""Writes the post url for hashtags to a file
This allows posts for a hashtag to be quickly looked up
"""
2020-04-04 10:05:27 +00:00
if tag['type'] != 'Hashtag':
2019-08-09 17:42:11 +00:00
return
2019-12-17 10:24:52 +00:00
2020-03-22 21:16:02 +00:00
# create hashtags directory
2020-04-04 10:05:27 +00:00
tagsDir = baseDir + '/tags'
2019-08-09 11:12:08 +00:00
if not os.path.isdir(tagsDir):
os.mkdir(tagsDir)
2020-04-04 10:05:27 +00:00
tagName = tag['name']
tagsFilename = tagsDir + '/' + tagName[1:] + '.txt'
tagline = newPostId + '\n'
2019-12-17 10:24:52 +00:00
if not os.path.isfile(tagsFilename):
# create a new tags index file
2020-04-04 10:05:27 +00:00
tagsFile = open(tagsFilename, "w+")
2019-12-17 10:24:52 +00:00
if tagsFile:
tagsFile.write(tagline)
tagsFile.close()
else:
# prepend to tags index file
if tagline not in open(tagsFilename).read():
try:
with open(tagsFilename, 'r+') as tagsFile:
2020-04-04 10:05:27 +00:00
content = tagsFile.read()
2019-12-17 10:24:52 +00:00
tagsFile.seek(0, 0)
tagsFile.write(tagline+content)
except Exception as e:
2020-04-04 10:05:27 +00:00
print('WARN: Failed to write entry to tags file ' +
tagsFilename + ' ' + str(e))
2019-08-09 11:12:08 +00:00
2020-04-04 10:05:27 +00:00
def addSchedulePost(baseDir: str, nickname: str, domain: str,
eventDateStr: str, postId: str) -> None:
2020-01-13 10:49:03 +00:00
"""Adds a scheduled post to the index
"""
2020-04-04 10:05:27 +00:00
handle = nickname + '@' + domain
scheduleIndexFilename = baseDir + '/accounts/' + handle + '/schedule.index'
2020-01-13 10:49:03 +00:00
2020-04-04 10:05:27 +00:00
indexStr = eventDateStr + ' ' + postId.replace('/', '#')
2020-01-13 10:49:03 +00:00
if os.path.isfile(scheduleIndexFilename):
if indexStr not in open(scheduleIndexFilename).read():
try:
with open(scheduleIndexFilename, 'r+') as scheduleFile:
2020-04-04 10:05:27 +00:00
content = scheduleFile.read()
2020-01-13 10:49:03 +00:00
scheduleFile.seek(0, 0)
2020-04-04 10:05:27 +00:00
scheduleFile.write(indexStr + '\n' + content)
2020-01-13 11:45:36 +00:00
print('DEBUG: scheduled post added to index')
2020-01-13 10:49:03 +00:00
except Exception as e:
2020-04-04 10:05:27 +00:00
print('WARN: Failed to write entry to scheduled posts index ' +
scheduleIndexFilename + ' ' + str(e))
2020-01-13 10:49:03 +00:00
else:
2020-04-04 10:05:27 +00:00
scheduleFile = open(scheduleIndexFilename, 'w')
2020-01-13 10:49:03 +00:00
if scheduleFile:
2020-04-04 10:05:27 +00:00
scheduleFile.write(indexStr + '\n')
2020-03-22 21:16:02 +00:00
scheduleFile.close()
2020-01-13 10:49:03 +00:00
2020-04-04 10:05:27 +00:00
def createPostBase(baseDir: str, nickname: str, domain: str, port: int,
toUrl: str, ccUrl: str, httpPrefix: str, content: str,
followersOnly: bool, saveToFile: bool, clientToServer: bool,
attachImageFilename: str,
mediaType: str, imageDescription: str,
useBlurhash: bool, isModerationReport: bool,
isArticle: bool, inReplyTo=None,
inReplyToAtomUri=None, subject=None, schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}:
2019-07-01 12:14:49 +00:00
"""Creates a message
2019-06-29 22:29:18 +00:00
"""
2020-04-04 10:05:27 +00:00
mentionedRecipients = \
getMentionedPeople(baseDir, httpPrefix, content, domain, False)
2019-08-19 09:37:14 +00:00
2020-04-04 10:05:27 +00:00
tags = []
hashtagsDict = {}
2019-07-15 14:41:15 +00:00
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domain = domain + ':' + str(port)
2019-11-01 10:19:21 +00:00
# add tags
2020-04-04 10:05:27 +00:00
content = \
addHtmlTags(baseDir, httpPrefix,
nickname, domain, content,
mentionedRecipients,
hashtagsDict, True)
2020-02-21 15:17:55 +00:00
# replace emoji with unicode
2020-04-04 10:05:27 +00:00
tags = []
for tagName, tag in hashtagsDict.items():
2020-02-21 15:17:55 +00:00
tags.append(tag)
# get list of tags
2020-04-04 10:05:27 +00:00
content = replaceEmojiFromTags(content, tags, 'content')
2020-02-21 15:17:55 +00:00
# remove replaced emoji
2020-04-04 10:05:27 +00:00
hashtagsDictCopy = hashtagsDict.copy()
for tagName, tag in hashtagsDictCopy.items():
2020-02-21 15:17:55 +00:00
if tag.get('name'):
if tag['name'].startswith(':'):
if tag['name'] not in content:
del hashtagsDict[tagName]
2020-04-04 10:05:27 +00:00
statusNumber, published = getStatusNumber()
newPostId = \
httpPrefix + '://' + domain + '/users/' + \
nickname + '/statuses/' + statusNumber
sensitive = False
summary = None
2019-06-29 10:23:40 +00:00
if subject:
2020-04-04 10:05:27 +00:00
summary = subject
sensitive = True
2019-07-15 14:41:15 +00:00
2020-04-04 10:05:27 +00:00
toRecipients = []
toCC = []
2019-08-11 18:32:29 +00:00
if toUrl:
if not isinstance(toUrl, str):
print('ERROR: toUrl is not a string')
return None
2020-04-04 10:05:27 +00:00
toRecipients = [toUrl]
2019-08-11 18:32:29 +00:00
2019-08-05 16:56:32 +00:00
# who to send to
2019-08-19 12:40:59 +00:00
if mentionedRecipients:
for mention in mentionedRecipients:
2019-11-04 12:09:59 +00:00
if mention not in toCC:
toCC.append(mention)
2019-08-09 11:12:08 +00:00
# create a list of hashtags
2019-09-05 11:37:41 +00:00
# Only posts which are #Public are searchable by hashtag
2019-08-10 16:55:17 +00:00
if hashtagsDict:
2020-04-04 10:05:27 +00:00
isPublic = False
2019-08-10 16:55:17 +00:00
for recipient in toRecipients:
if recipient.endswith('#Public'):
2020-04-04 10:05:27 +00:00
isPublic = True
2019-08-10 16:55:17 +00:00
break
2020-04-04 10:05:27 +00:00
for tagName, tag in hashtagsDict.items():
2019-08-09 11:12:08 +00:00
tags.append(tag)
if isPublic:
2020-04-04 10:05:27 +00:00
updateHashtagsIndex(baseDir, tag, newPostId)
print('Content tags: ' + str(tags))
if inReplyTo and not sensitive:
# locate the post which this is a reply to and check if
# it has a content warning. If it does then reproduce
# the same warning
2020-04-04 10:05:27 +00:00
replyPostFilename = \
locatePost(baseDir, nickname, domain, inReplyTo)
if replyPostFilename:
2020-04-04 10:05:27 +00:00
replyToJson = loadJson(replyPostFilename)
if replyToJson:
if replyToJson.get('object'):
if replyToJson['object'].get('sensitive'):
if replyToJson['object']['sensitive']:
2020-04-04 10:05:27 +00:00
sensitive = True
if replyToJson['object'].get('summary'):
2020-04-04 10:05:27 +00:00
summary = replyToJson['object']['summary']
eventDateStr = None
2019-10-10 13:12:13 +00:00
if eventDate:
2020-04-04 10:05:27 +00:00
eventName = summary
2019-10-10 13:12:13 +00:00
if not eventName:
2020-04-04 10:05:27 +00:00
eventName = content
eventDateStr = eventDate
2019-10-10 13:12:13 +00:00
if eventTime:
2019-10-10 13:24:29 +00:00
if eventTime.endswith('Z'):
2020-04-04 10:05:27 +00:00
eventDateStr = eventDate + 'T' + eventTime
2019-10-10 13:24:29 +00:00
else:
2020-04-04 10:05:27 +00:00
eventDateStr = eventDate + 'T' + eventTime + \
':00' + strftime("%z", gmtime())
2019-10-10 13:12:13 +00:00
else:
2020-04-04 10:05:27 +00:00
eventDateStr = eventDate + 'T12:00:00Z'
2020-01-12 13:19:03 +00:00
if not schedulePost:
tags.append({
2019-10-10 13:12:13 +00:00
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Event",
"name": eventName,
"startTime": eventDateStr,
"endTime": eventDateStr
2020-01-12 13:19:03 +00:00
})
2020-01-12 13:16:02 +00:00
if location:
tags.append({
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Place",
"name": location
})
2019-10-19 15:59:49 +00:00
2020-04-04 10:05:27 +00:00
postContext = [
2019-10-19 15:59:49 +00:00
'https://www.w3.org/ns/activitystreams',
{
'Hashtag': 'as:Hashtag',
'sensitive': 'as:sensitive',
'toot': 'http://joinmastodon.org/ns#',
'votersCount': 'toot:votersCount'
}
]
2020-03-22 21:16:02 +00:00
2020-06-07 13:35:22 +00:00
# make sure that CC doesn't also contain a To address
# eg. To: [ "https://mydomain/users/foo/followers" ]
# CC: [ "X", "Y", "https://mydomain/users/foo", "Z" ]
removeFromCC = []
for ccRecipient in toCC:
for sendToActor in toRecipients:
2020-06-07 18:02:20 +00:00
if ccRecipient in sendToActor and \
ccRecipient not in removeFromCC:
removeFromCC.append(ccRecipient)
break
2020-06-07 13:35:22 +00:00
for ccRemoval in removeFromCC:
toCC.remove(ccRemoval)
2019-07-03 15:10:18 +00:00
if not clientToServer:
2020-04-04 10:05:27 +00:00
actorUrl = httpPrefix + '://' + domain + '/users/' + nickname
2019-07-06 10:33:57 +00:00
2019-07-07 11:53:32 +00:00
# if capabilities have been granted for this actor
# then get the corresponding id
2020-04-04 10:05:27 +00:00
capabilityIdList = []
ocapFilename = getOcapFilename(baseDir, nickname, domain,
toUrl, 'granted')
2019-08-18 20:47:12 +00:00
if ocapFilename:
if os.path.isfile(ocapFilename):
2020-04-04 10:05:27 +00:00
oc = loadJson(ocapFilename)
2019-10-22 11:55:06 +00:00
if oc:
if oc.get('id'):
2020-04-04 10:05:27 +00:00
capabilityIdList = [oc['id']]
idStr = \
httpPrefix + '://' + domain + '/users/' + nickname + \
'/statuses/' + statusNumber + '/replies'
newPost = {
2020-05-03 14:39:21 +00:00
'@context': postContext,
2019-07-03 15:10:18 +00:00
'id': newPostId+'/activity',
2019-07-08 13:30:04 +00:00
'capability': capabilityIdList,
2019-07-03 15:10:18 +00:00
'type': 'Create',
2019-07-06 10:33:57 +00:00
'actor': actorUrl,
2019-07-03 15:10:18 +00:00
'published': published,
2019-11-04 12:09:59 +00:00
'to': toRecipients,
'cc': toCC,
2019-07-03 15:10:18 +00:00
'object': {
'id': newPostId,
'type': 'Note',
'summary': summary,
'inReplyTo': inReplyTo,
'published': published,
2019-07-03 19:00:03 +00:00
'url': httpPrefix+'://'+domain+'/@'+nickname+'/'+statusNumber,
'attributedTo': httpPrefix+'://'+domain+'/users/'+nickname,
2019-08-05 16:56:32 +00:00
'to': toRecipients,
2019-11-04 12:09:59 +00:00
'cc': toCC,
2019-07-03 15:10:18 +00:00
'sensitive': sensitive,
2019-07-03 22:59:56 +00:00
'atomUri': newPostId,
2019-07-03 15:10:18 +00:00
'inReplyToAtomUri': inReplyToAtomUri,
2020-05-03 14:39:21 +00:00
'mediaType': 'text/html',
2019-07-03 15:10:18 +00:00
'content': content,
'contentMap': {
'en': content
},
'attachment': [],
2019-08-09 11:12:08 +00:00
'tag': tags,
2019-07-11 13:46:12 +00:00
'replies': {
2020-04-04 10:05:27 +00:00
'id': idStr,
2019-07-11 13:46:12 +00:00
'type': 'Collection',
'first': {
'type': 'CollectionPage',
2020-04-04 10:05:27 +00:00
'partOf': idStr,
2019-07-11 13:46:12 +00:00
'items': []
}
}
2019-07-03 15:10:18 +00:00
}
}
2019-07-12 19:08:46 +00:00
if attachImageFilename:
2020-04-04 10:05:27 +00:00
newPost['object'] = \
attachMedia(baseDir, httpPrefix, domain, port,
newPost['object'], attachImageFilename,
mediaType, imageDescription, useBlurhash)
2019-07-03 15:10:18 +00:00
else:
2020-04-04 10:05:27 +00:00
idStr = \
httpPrefix + '://' + domain + '/users/' + nickname + \
'/statuses/' + statusNumber + '/replies'
newPost = {
2019-10-19 15:59:49 +00:00
"@context": postContext,
2019-07-03 15:10:18 +00:00
'id': newPostId,
'type': 'Note',
'summary': summary,
'inReplyTo': inReplyTo,
'published': published,
2019-07-03 19:00:03 +00:00
'url': httpPrefix+'://'+domain+'/@'+nickname+'/'+statusNumber,
'attributedTo': httpPrefix+'://'+domain+'/users/'+nickname,
2019-08-05 16:56:32 +00:00
'to': toRecipients,
2019-11-04 12:09:59 +00:00
'cc': toCC,
2019-07-03 15:10:18 +00:00
'sensitive': sensitive,
2019-07-03 22:59:56 +00:00
'atomUri': newPostId,
2019-07-03 15:10:18 +00:00
'inReplyToAtomUri': inReplyToAtomUri,
2020-05-03 14:40:59 +00:00
'mediaType': 'text/html',
2019-07-03 15:10:18 +00:00
'content': content,
'contentMap': {
'en': content
},
'attachment': [],
2019-08-09 11:12:08 +00:00
'tag': tags,
2019-07-12 19:08:46 +00:00
'replies': {
2020-04-04 10:05:27 +00:00
'id': idStr,
2019-07-12 19:08:46 +00:00
'type': 'Collection',
'first': {
'type': 'CollectionPage',
2020-04-04 10:05:27 +00:00
'partOf': idStr,
2019-07-12 19:08:46 +00:00
'items': []
}
}
2019-06-28 18:55:29 +00:00
}
2019-07-12 19:08:46 +00:00
if attachImageFilename:
2020-04-04 10:05:27 +00:00
newPost = \
attachMedia(baseDir, httpPrefix, domain, port,
newPost, attachImageFilename,
mediaType, imageDescription, useBlurhash)
2019-07-01 12:14:49 +00:00
if ccUrl:
2020-04-04 10:05:27 +00:00
if len(ccUrl) > 0:
newPost['cc'] = [ccUrl]
2019-07-19 18:12:50 +00:00
if newPost.get('object'):
2020-04-04 10:05:27 +00:00
newPost['object']['cc'] = [ccUrl]
2019-08-11 20:38:10 +00:00
# if this is a moderation report then add a status
if isModerationReport:
2019-08-12 13:22:17 +00:00
# add status
2019-08-11 20:38:10 +00:00
if newPost.get('object'):
2020-04-04 10:05:27 +00:00
newPost['object']['moderationStatus'] = 'pending'
2019-08-11 20:38:10 +00:00
else:
2020-04-04 10:05:27 +00:00
newPost['moderationStatus'] = 'pending'
2019-08-12 13:22:17 +00:00
# save to index file
2020-04-04 10:05:27 +00:00
moderationIndexFile = baseDir + '/accounts/moderation.txt'
modFile = open(moderationIndexFile, "a+")
2019-08-12 13:22:17 +00:00
if modFile:
2020-04-04 10:05:27 +00:00
modFile.write(newPostId + '\n')
2019-08-12 13:22:17 +00:00
modFile.close()
2019-08-11 20:38:10 +00:00
2020-05-03 12:52:13 +00:00
# If a patch has been posted - i.e. the output from
# git format-patch - then convert the activitypub type
2020-05-03 12:52:13 +00:00
convertPostToPatch(baseDir, nickname, domain, newPost)
2020-01-12 20:53:00 +00:00
if schedulePost:
2020-03-22 21:16:02 +00:00
if eventDate and eventTime:
2020-01-12 20:53:00 +00:00
# add an item to the scheduled post index file
2020-04-04 10:05:27 +00:00
addSchedulePost(baseDir, nickname, domain, eventDateStr, newPostId)
savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'scheduled')
2020-01-12 20:53:00 +00:00
else:
2020-04-04 10:05:27 +00:00
print('Unable to create scheduled post without ' +
'date and time values')
2020-01-12 20:53:00 +00:00
return newPost
elif saveToFile:
2020-02-24 22:34:54 +00:00
if not isArticle:
2020-04-04 10:05:27 +00:00
savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'outbox')
2020-02-24 22:34:54 +00:00
else:
2020-04-04 10:05:27 +00:00
savePostToBox(baseDir, httpPrefix, newPostId,
nickname, domain, newPost, 'tlblogs')
2019-06-28 18:55:29 +00:00
return newPost
2019-06-29 10:08:59 +00:00
2020-04-04 10:05:27 +00:00
def outboxMessageCreateWrap(httpPrefix: str,
nickname: str, domain: str, port: int,
2019-07-06 17:00:22 +00:00
messageJson: {}) -> {}:
2019-07-03 21:37:46 +00:00
"""Wraps a received message in a Create
https://www.w3.org/TR/activitypub/#object-without-create
"""
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domain = domain + ':' + str(port)
statusNumber, published = getStatusNumber()
2019-07-03 21:37:46 +00:00
if messageJson.get('published'):
2020-04-04 10:05:27 +00:00
published = messageJson['published']
newPostId = \
httpPrefix + '://' + domain + '/users/' + nickname + \
'/statuses/' + statusNumber
cc = []
2019-07-03 21:37:46 +00:00
if messageJson.get('cc'):
2020-04-04 10:05:27 +00:00
cc = messageJson['cc']
capabilityUrl = []
newPost = {
2019-08-18 11:07:06 +00:00
"@context": "https://www.w3.org/ns/activitystreams",
2019-07-03 21:37:46 +00:00
'id': newPostId+'/activity',
2019-07-05 20:46:47 +00:00
'capability': capabilityUrl,
2019-07-03 21:37:46 +00:00
'type': 'Create',
'actor': httpPrefix+'://'+domain+'/users/'+nickname,
'published': published,
'to': messageJson['to'],
'cc': cc,
'object': messageJson
}
2020-04-04 10:05:27 +00:00
newPost['object']['id'] = newPost['id']
newPost['object']['url'] = \
httpPrefix + '://' + domain + '/@' + nickname + '/' + statusNumber
newPost['object']['atomUri'] = \
httpPrefix + '://' + domain + '/users/' + nickname + \
'/statuses/' + statusNumber
2019-07-03 21:37:46 +00:00
return newPost
2020-04-04 10:05:27 +00:00
2019-07-08 13:30:04 +00:00
def postIsAddressedToFollowers(baseDir: str,
2020-04-04 10:05:27 +00:00
nickname: str, domain: str, port: int,
2019-12-12 09:58:06 +00:00
httpPrefix: str,
2019-07-14 16:57:06 +00:00
postJsonObject: {}) -> bool:
2019-07-08 13:30:04 +00:00
"""Returns true if the given post is addressed to followers of the nickname
"""
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domain = domain + ':' + str(port)
2019-07-08 13:30:04 +00:00
2019-07-14 16:57:06 +00:00
if not postJsonObject.get('object'):
2019-07-08 13:30:04 +00:00
return False
2020-04-04 10:05:27 +00:00
toList = []
ccList = []
if postJsonObject['type'] != 'Update' and \
2019-08-20 21:09:56 +00:00
isinstance(postJsonObject['object'], dict):
2019-11-04 12:46:51 +00:00
if postJsonObject['object'].get('to'):
2020-04-04 10:05:27 +00:00
toList = postJsonObject['object']['to']
2019-07-16 19:07:45 +00:00
if postJsonObject['object'].get('cc'):
2020-04-04 10:05:27 +00:00
ccList = postJsonObject['object']['cc']
2019-07-16 19:07:45 +00:00
else:
2019-11-04 12:46:51 +00:00
if postJsonObject.get('to'):
2020-04-04 10:05:27 +00:00
toList = postJsonObject['to']
2019-07-16 19:07:45 +00:00
if postJsonObject.get('cc'):
2020-04-04 10:05:27 +00:00
ccList = postJsonObject['cc']
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
followersUrl = httpPrefix + '://' + domain + '/users/' + \
nickname + '/followers'
2019-07-08 13:30:04 +00:00
# does the followers url exist in 'to' or 'cc' lists?
2020-04-04 10:05:27 +00:00
addressedToFollowers = False
2019-07-16 19:07:45 +00:00
if followersUrl in toList:
2020-04-04 10:05:27 +00:00
addressedToFollowers = True
2019-11-04 12:46:51 +00:00
elif followersUrl in ccList:
2020-04-04 10:05:27 +00:00
addressedToFollowers = True
2019-07-08 13:30:04 +00:00
return addressedToFollowers
2020-04-04 10:05:27 +00:00
def postIsAddressedToPublic(baseDir: str, postJsonObject: {}) -> bool:
2019-07-15 17:22:51 +00:00
"""Returns true if the given post is addressed to public
"""
if not postJsonObject.get('object'):
return False
if not postJsonObject['object'].get('to'):
return False
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
publicUrl = 'https://www.w3.org/ns/activitystreams#Public'
2019-07-15 17:22:51 +00:00
# does the public url exist in 'to' or 'cc' lists?
2020-04-04 10:05:27 +00:00
addressedToPublic = False
2019-07-15 17:22:51 +00:00
if publicUrl in postJsonObject['object']['to']:
2020-04-04 10:05:27 +00:00
addressedToPublic = True
2019-07-15 17:22:51 +00:00
if not addressedToPublic:
if not postJsonObject['object'].get('cc'):
return False
if publicUrl in postJsonObject['object']['cc']:
2020-04-04 10:05:27 +00:00
addressedToPublic = True
2019-07-15 17:22:51 +00:00
return addressedToPublic
2020-04-04 10:05:27 +00:00
def createPublicPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}:
2019-07-27 22:48:34 +00:00
"""Public post
2019-06-30 10:14:02 +00:00
"""
2020-04-04 10:05:27 +00:00
domainFull = domain
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
return createPostBase(baseDir, nickname, domain, port,
'https://www.w3.org/ns/activitystreams#Public',
httpPrefix + '://' + domainFull + '/users/' +
nickname + '/followers',
httpPrefix, content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location)
def createBlogPost(baseDir: str,
nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}:
blog = \
createPublicPost(baseDir,
nickname, domain, port, httpPrefix,
content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
inReplyTo, inReplyToAtomUri, subject,
schedulePost,
eventDate, eventTime, location)
blog['object']['type'] = 'Article'
2020-02-24 13:32:19 +00:00
return blog
2020-03-22 21:16:02 +00:00
2020-02-24 13:32:19 +00:00
2019-11-25 22:34:26 +00:00
def createQuestionPost(baseDir: str,
2020-04-04 10:05:27 +00:00
nickname: str, domain: str, port: int, httpPrefix: str,
content: str, qOptions: [],
followersOnly: bool, saveToFile: bool,
clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
subject: str, durationDays: int) -> {}:
2019-11-25 22:34:26 +00:00
"""Question post with multiple choice options
"""
2020-04-04 10:05:27 +00:00
domainFull = domain
2019-11-25 22:34:26 +00:00
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
2019-11-25 22:34:26 +00:00
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
messageJson = \
createPostBase(baseDir, nickname, domain, port,
'https://www.w3.org/ns/activitystreams#Public',
httpPrefix + '://' + domainFull + '/users/' +
nickname + '/followers',
httpPrefix, content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, False, None, None, subject,
False, None, None, None)
messageJson['object']['type'] = 'Question'
messageJson['object']['oneOf'] = []
messageJson['object']['votersCount'] = 0
currTime = datetime.datetime.utcnow()
daysSinceEpoch = \
int((currTime - datetime.datetime(1970, 1, 1)).days + durationDays)
endTime = datetime.datetime(1970, 1, 1) + \
datetime.timedelta(daysSinceEpoch)
messageJson['object']['endTime'] = endTime.strftime("%Y-%m-%dT%H:%M:%SZ")
2019-11-25 22:34:26 +00:00
for questionOption in qOptions:
2019-11-29 13:54:25 +00:00
messageJson['object']['oneOf'].append({
2019-11-25 22:34:26 +00:00
"type": "Note",
"name": questionOption,
"replies": {
"type": "Collection",
"totalItems": 0
}
})
return messageJson
2020-02-24 13:32:19 +00:00
2019-07-28 11:08:14 +00:00
def createUnlistedPost(baseDir: str,
2020-04-04 10:05:27 +00:00
nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}:
2019-07-28 11:08:14 +00:00
"""Unlisted post. This has the #Public and followers links inverted.
"""
2020-04-04 10:05:27 +00:00
domainFull = domain
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
return createPostBase(baseDir, nickname, domain, port,
httpPrefix + '://' + domainFull + '/users/' +
nickname + '/followers',
'https://www.w3.org/ns/activitystreams#Public',
httpPrefix, content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location)
2019-07-28 11:08:14 +00:00
2019-07-27 22:48:34 +00:00
def createFollowersOnlyPost(baseDir: str,
2020-04-04 10:05:27 +00:00
nickname: str, domain: str, port: int,
httpPrefix: str,
content: str, followersOnly: bool,
saveToFile: bool,
clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None,
subject=None, schedulePost=False,
eventDate=None, eventTime=None,
location=None) -> {}:
2019-07-27 22:48:34 +00:00
"""Followers only post
"""
2020-04-04 10:05:27 +00:00
domainFull = domain
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
return createPostBase(baseDir, nickname, domain, port,
httpPrefix + '://' + domainFull + '/users/' +
nickname + '/followers',
2019-07-27 22:48:34 +00:00
None,
2020-04-04 10:05:27 +00:00
httpPrefix, content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location)
def getMentionedPeople(baseDir: str, httpPrefix: str,
content: str, domain: str, debug: bool) -> []:
2019-07-27 22:48:34 +00:00
"""Extracts a list of mentioned actors from the given message content
"""
if '@' not in content:
return None
2020-04-04 10:05:27 +00:00
mentions = []
words = content.split(' ')
2019-07-27 22:48:34 +00:00
for wrd in words:
if wrd.startswith('@'):
2020-04-04 10:05:27 +00:00
handle = wrd[1:]
2019-08-19 09:11:25 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: mentioned handle ' + handle)
2019-07-27 22:48:34 +00:00
if '@' not in handle:
2020-04-04 10:05:27 +00:00
handle = handle + '@' + domain
if not os.path.isdir(baseDir + '/accounts/' + handle):
2019-07-27 22:48:34 +00:00
continue
else:
2020-04-04 10:05:27 +00:00
externalDomain = handle.split('@')[1]
if not ('.' in externalDomain or
externalDomain == 'localhost'):
2019-07-27 22:48:34 +00:00
continue
2020-04-04 10:05:27 +00:00
mentionedNickname = handle.split('@')[0]
2020-05-22 11:32:38 +00:00
mentionedDomain = handle.split('@')[1].strip('\n').strip('\r')
2019-08-23 13:47:29 +00:00
if ':' in mentionedDomain:
2020-04-04 10:05:27 +00:00
mentionedDomain = mentionedDomain.split(':')[0]
if not validNickname(mentionedDomain, mentionedNickname):
2019-07-27 22:48:34 +00:00
continue
2020-04-04 10:05:27 +00:00
actor = \
httpPrefix + '://' + handle.split('@')[1] + \
'/users/' + mentionedNickname
2019-07-27 22:48:34 +00:00
mentions.append(actor)
2019-08-19 09:16:33 +00:00
return mentions
2019-07-27 22:48:34 +00:00
2020-04-04 10:05:27 +00:00
2019-07-27 22:48:34 +00:00
def createDirectMessagePost(baseDir: str,
2020-04-04 10:05:27 +00:00
nickname: str, domain: str, port: int,
httpPrefix: str,
content: str, followersOnly: bool,
saveToFile: bool, clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
inReplyTo=None, inReplyToAtomUri=None,
subject=None, debug=False,
schedulePost=False,
eventDate=None, eventTime=None,
location=None) -> {}:
2019-07-27 22:48:34 +00:00
"""Direct Message post
"""
2020-04-04 10:05:27 +00:00
mentionedPeople = \
getMentionedPeople(baseDir, httpPrefix, content, domain, debug)
2019-08-19 09:11:25 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('mentionedPeople: ' + str(mentionedPeople))
2019-07-27 22:48:34 +00:00
if not mentionedPeople:
return None
2020-04-04 10:05:27 +00:00
postTo = None
postCc = None
messageJson = \
createPostBase(baseDir, nickname, domain, port,
postTo, postCc,
httpPrefix, content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location)
# mentioned recipients go into To rather than Cc
2020-04-04 10:05:27 +00:00
messageJson['to'] = messageJson['object']['cc']
2020-06-24 12:33:06 +00:00
messageJson['object']['to'] = messageJson['to']
messageJson['cc'] = []
messageJson['object']['cc'] = []
if schedulePost:
savePostToBox(baseDir, httpPrefix, messageJson['object']['id'],
nickname, domain, messageJson, 'scheduled')
return messageJson
2019-08-11 11:25:27 +00:00
def createReportPost(baseDir: str,
2020-04-04 10:05:27 +00:00
nickname: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
debug: bool, subject=None) -> {}:
2019-08-11 11:25:27 +00:00
"""Send a report to moderators
"""
2020-04-04 10:05:27 +00:00
domainFull = domain
2019-08-11 11:25:27 +00:00
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
2019-08-11 11:25:27 +00:00
2019-08-11 11:33:29 +00:00
# add a title to distinguish moderation reports from other posts
2020-04-04 10:05:27 +00:00
reportTitle = 'Moderation Report'
2019-08-11 11:33:29 +00:00
if not subject:
2020-04-04 10:05:27 +00:00
subject = reportTitle
2019-08-11 11:33:29 +00:00
else:
if not subject.startswith(reportTitle):
2020-04-04 10:05:27 +00:00
subject = reportTitle + ': ' + subject
2019-08-11 11:33:29 +00:00
2019-08-11 13:02:36 +00:00
# create the list of moderators from the moderators file
2020-04-04 10:05:27 +00:00
moderatorsList = []
moderatorsFile = baseDir + '/accounts/moderators.txt'
2019-08-11 11:25:27 +00:00
if os.path.isfile(moderatorsFile):
2020-04-04 10:05:27 +00:00
with open(moderatorsFile, "r") as fileHandler:
2019-08-11 11:25:27 +00:00
for line in fileHandler:
2020-05-22 11:32:38 +00:00
line = line.strip('\n').strip('\r')
2019-08-11 11:25:27 +00:00
if line.startswith('#'):
continue
if line.startswith('/users/'):
2020-04-04 10:05:27 +00:00
line = line.replace('users', '')
2019-08-11 11:25:27 +00:00
if line.startswith('@'):
2020-04-04 10:05:27 +00:00
line = line[1:]
2019-08-11 11:25:27 +00:00
if '@' in line:
2020-04-04 10:05:27 +00:00
moderatorActor = httpPrefix + '://' + domainFull + \
'/users/' + line.split('@')[0]
if moderatorActor not in moderatorsList:
2019-08-11 11:25:27 +00:00
moderatorsList.append(moderatorActor)
continue
if line.startswith('http') or line.startswith('dat'):
# must be a local address - no remote moderators
2020-04-04 10:05:27 +00:00
if '://' + domainFull + '/' in line:
2019-08-11 11:25:27 +00:00
if line not in moderatorsList:
moderatorsList.append(line)
else:
if '/' not in line:
2020-04-04 10:05:27 +00:00
moderatorActor = httpPrefix + '://' + domainFull + \
'/users/' + line
2019-08-11 11:25:27 +00:00
if moderatorActor not in moderatorsList:
moderatorsList.append(moderatorActor)
2020-04-04 10:05:27 +00:00
if len(moderatorsList) == 0:
2019-08-11 11:25:27 +00:00
# if there are no moderators then the admin becomes the moderator
2020-04-04 10:05:27 +00:00
adminNickname = getConfigParam(baseDir, 'admin')
2019-08-11 11:25:27 +00:00
if adminNickname:
2020-04-04 10:05:27 +00:00
moderatorsList.append(httpPrefix + '://' + domainFull +
'/users/' + adminNickname)
2019-08-11 11:25:27 +00:00
if not moderatorsList:
return None
if debug:
print('DEBUG: Sending report to moderators')
print(str(moderatorsList))
2020-04-04 10:05:27 +00:00
postTo = moderatorsList
postCc = None
postJsonObject = None
2020-03-22 21:16:02 +00:00
for toUrl in postTo:
2019-11-16 22:09:54 +00:00
# who is this report going to?
2020-04-04 10:05:27 +00:00
toNickname = toUrl.split('/users/')[1]
handle = toNickname + '@' + domain
postJsonObject = \
createPostBase(baseDir, nickname, domain, port,
toUrl, postCc,
httpPrefix, content, followersOnly, saveToFile,
clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
True, False, None, None, subject,
False, None, None, None)
if not postJsonObject:
continue
2019-11-16 18:14:00 +00:00
2019-11-16 18:11:30 +00:00
# update the inbox index with the report filename
2020-04-04 10:05:27 +00:00
# indexFilename=baseDir+'/accounts/'+handle+'/inbox.index'
# indexEntry=postJsonObject['id'].replace('/activity','').replace('/','#')+'.json'
# if indexEntry not in open(indexFilename).read():
# try:
# with open(indexFilename, 'a+') as fp:
# fp.write(indexEntry)
# except:
# pass
2019-11-16 18:11:30 +00:00
# save a notification file so that the moderator
# knows something new has appeared
2020-04-04 10:05:27 +00:00
newReportFile = baseDir + '/accounts/' + handle + '/.newReport'
if os.path.isfile(newReportFile):
continue
try:
with open(newReportFile, 'w') as fp:
2020-04-04 10:05:27 +00:00
fp.write(toUrl + '/moderation')
except BaseException:
pass
2019-08-11 18:32:29 +00:00
return postJsonObject
2019-08-11 11:25:27 +00:00
2020-04-04 10:05:27 +00:00
def threadSendPost(session, postJsonStr: str, federationList: [],
inboxUrl: str, baseDir: str,
signatureHeaderJson: {}, postLog: [],
debug: bool) -> None:
"""Sends a with retries
2019-06-30 13:38:01 +00:00
"""
2020-04-04 10:05:27 +00:00
tries = 0
sendIntervalSec = 30
2019-06-30 13:38:01 +00:00
for attempt in range(20):
2020-04-04 10:05:27 +00:00
postResult = None
unauthorized = False
2019-10-14 21:05:14 +00:00
try:
2020-04-04 10:05:27 +00:00
postResult, unauthorized = \
postJsonString(session, postJsonStr, federationList,
inboxUrl, signatureHeaderJson,
"inbox:write", debug)
2019-10-14 21:05:14 +00:00
except Exception as e:
2020-04-04 10:05:27 +00:00
print('ERROR: postJsonString failed ' + str(e))
if unauthorized:
2019-10-23 18:44:03 +00:00
print(postJsonStr)
print('threadSendPost: Post is unauthorized')
break
2019-08-21 21:05:37 +00:00
if postResult:
2020-04-04 10:05:27 +00:00
logStr = 'Success on try ' + str(tries) + ': ' + postJsonStr
2019-08-21 21:05:37 +00:00
else:
2020-04-04 10:05:27 +00:00
logStr = 'Retry ' + str(tries) + ': ' + postJsonStr
2019-08-21 21:05:37 +00:00
postLog.append(logStr)
# keep the length of the log finite
# Don't accumulate massive files on systems with limited resources
2020-04-04 10:05:27 +00:00
while len(postLog) > 16:
2019-09-01 10:11:06 +00:00
postLog.pop(0)
2019-10-16 11:27:43 +00:00
if debug:
# save the log file
2020-04-04 10:05:27 +00:00
postLogFilename = baseDir + '/post.log'
2019-10-16 11:27:43 +00:00
with open(postLogFilename, "a+") as logFile:
2020-04-04 10:05:27 +00:00
logFile.write(logStr + '\n')
2019-08-21 21:05:37 +00:00
2019-06-30 13:38:01 +00:00
if postResult:
2019-07-06 13:49:25 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: successful json post to ' + inboxUrl)
2019-06-30 13:38:01 +00:00
# our work here is done
2019-06-30 13:20:23 +00:00
break
2019-07-06 13:49:25 +00:00
if debug:
2019-08-18 09:58:28 +00:00
print(postJsonStr)
2020-04-04 10:05:27 +00:00
print('DEBUG: json post to ' + inboxUrl +
' failed. Waiting for ' +
str(sendIntervalSec) + ' seconds.')
time.sleep(sendIntervalSec)
2020-04-04 10:05:27 +00:00
tries += 1
def sendPost(projectVersion: str,
session, baseDir: str, nickname: str, domain: str, port: int,
toNickname: str, toDomain: str, toPort: int, cc: str,
httpPrefix: str, content: str, followersOnly: bool,
saveToFile: bool, clientToServer: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
federationList: [], sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
isArticle: bool,
debug=False, inReplyTo=None,
inReplyToAtomUri=None, subject=None) -> int:
2019-06-30 10:14:02 +00:00
"""Post to another inbox
"""
2020-04-04 10:05:27 +00:00
withDigest = True
2019-07-01 09:31:02 +00:00
2020-04-04 10:05:27 +00:00
if toNickname == 'inbox':
# shared inbox actor on @domain@domain
2020-04-04 10:05:27 +00:00
toNickname = toDomain
if toPort:
2020-04-04 10:05:27 +00:00
if toPort != 80 and toPort != 443:
if ':' not in toDomain:
2020-04-04 10:05:27 +00:00
toDomain = toDomain + ':' + str(toPort)
2019-06-30 22:56:37 +00:00
2020-04-04 10:05:27 +00:00
handle = httpPrefix + '://' + toDomain + '/@' + toNickname
2019-06-30 22:56:37 +00:00
# lookup the inbox for the To handle
2020-04-04 10:05:27 +00:00
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
domain, projectVersion)
2019-06-30 10:14:02 +00:00
if not wfRequest:
return 1
2020-06-23 10:41:12 +00:00
if not isinstance(wfRequest, dict):
print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
return 1
2019-06-30 10:14:02 +00:00
2019-07-05 22:13:20 +00:00
if not clientToServer:
2020-04-04 10:05:27 +00:00
postToBox = 'inbox'
2019-07-05 22:13:20 +00:00
else:
2020-04-04 10:05:27 +00:00
postToBox = 'outbox'
2020-02-24 22:34:54 +00:00
if isArticle:
2020-04-04 10:05:27 +00:00
postToBox = 'tlblogs'
2019-07-05 22:13:20 +00:00
2019-06-30 22:56:37 +00:00
# get the actor inbox for the To handle
2020-04-04 10:05:27 +00:00
(inboxUrl, pubKeyId, pubKey,
toPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain, postToBox)
2019-07-05 14:39:24 +00:00
# If there are more than one followers on the target domain
2019-07-16 10:19:04 +00:00
# then send to the shared inbox indead of the individual inbox
2020-04-04 10:05:27 +00:00
if nickname == 'capabilities':
inboxUrl = capabilityAcquisition
2019-07-05 22:13:20 +00:00
if not capabilityAcquisition:
return 2
2020-03-22 21:16:02 +00:00
2019-06-30 10:14:02 +00:00
if not inboxUrl:
return 3
2019-07-05 22:13:20 +00:00
if not pubKey:
2019-06-30 10:14:02 +00:00
return 4
2019-07-05 22:13:20 +00:00
if not toPersonId:
return 5
# sharedInbox and capabilities are optional
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
postJsonObject = \
createPostBase(baseDir, nickname, domain, port,
toPersonId, cc, httpPrefix, content,
followersOnly, saveToFile, clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, isArticle, inReplyTo,
inReplyToAtomUri, subject,
False, None, None, None)
2019-06-30 10:14:02 +00:00
2019-06-30 22:56:37 +00:00
# get the senders private key
2020-04-04 10:05:27 +00:00
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private')
if len(privateKeyPem) == 0:
2019-07-05 22:13:20 +00:00
return 6
2019-06-30 10:14:02 +00:00
2019-07-05 22:13:20 +00:00
if toDomain not in inboxUrl:
return 7
2020-04-04 10:05:27 +00:00
postPath = inboxUrl.split(toDomain, 1)[1]
if not postJsonObject.get('signature'):
try:
signedPostJsonObject = jsonldSign(postJsonObject, privateKeyPem)
postJsonObject = signedPostJsonObject
except BaseException:
print('WARN: failed to JSON-LD sign post')
pass
2020-06-15 13:08:19 +00:00
# convert json to string so that there are no
# subsequent conversions after creating message body digest
2020-04-04 10:05:27 +00:00
postJsonStr = json.dumps(postJsonObject)
# construct the http header, including the message body digest
2020-04-04 10:05:27 +00:00
signatureHeaderJson = \
createSignedHeader(privateKeyPem, nickname, domain, port,
toDomain, toPort,
postPath, httpPrefix, withDigest, postJsonStr)
2019-07-05 18:57:19 +00:00
# Keep the number of threads being used small
2020-04-04 10:05:27 +00:00
while len(sendThreads) > 1000:
2019-10-16 14:46:29 +00:00
print('WARN: Maximum threads reached - killing send thread')
2019-07-05 18:57:19 +00:00
sendThreads[0].kill()
sendThreads.pop(0)
2019-10-16 14:46:29 +00:00
print('WARN: thread killed')
2020-04-04 10:05:27 +00:00
thr = \
threadWithTrace(target=threadSendPost,
args=(session,
postJsonStr,
federationList,
inboxUrl, baseDir,
signatureHeaderJson.copy(),
2019-12-12 09:58:06 +00:00
postLog,
2020-04-04 10:05:27 +00:00
debug), daemon=True)
2019-07-05 18:57:19 +00:00
sendThreads.append(thr)
thr.start()
return 0
2020-04-04 10:05:27 +00:00
def sendPostViaServer(projectVersion: str,
baseDir: str, session, fromNickname: str, password: str,
fromDomain: str, fromPort: int,
toNickname: str, toDomain: str, toPort: int, cc: str,
httpPrefix: str, content: str, followersOnly: bool,
attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool,
cachedWebfingers: {}, personCache: {},
isArticle: bool, debug=False, inReplyTo=None,
inReplyToAtomUri=None, subject=None) -> int:
2019-07-16 10:19:04 +00:00
"""Send a post via a proxy (c2s)
"""
2019-07-16 11:33:40 +00:00
if not session:
print('WARN: No session for sendPostViaServer')
return 6
2019-07-16 10:19:04 +00:00
if toPort:
2020-04-04 10:05:27 +00:00
if toPort != 80 and toPort != 443:
if ':' not in fromDomain:
2020-04-04 10:05:27 +00:00
fromDomain = fromDomain + ':' + str(fromPort)
2019-07-16 10:19:04 +00:00
2020-04-04 10:05:27 +00:00
handle = httpPrefix + '://' + fromDomain + '/@' + fromNickname
2019-07-16 10:19:04 +00:00
# lookup the inbox for the To handle
2020-04-04 10:05:27 +00:00
wfRequest = \
webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
fromDomain, projectVersion)
2019-07-16 10:19:04 +00:00
if not wfRequest:
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: webfinger failed for ' + handle)
2019-07-16 10:19:04 +00:00
return 1
2020-06-23 10:41:12 +00:00
if not isinstance(wfRequest, dict):
print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
return 1
2019-07-16 10:19:04 +00:00
2020-04-04 10:05:27 +00:00
postToBox = 'outbox'
2020-02-24 22:34:54 +00:00
if isArticle:
2020-04-04 10:05:27 +00:00
postToBox = 'tlblogs'
2019-07-16 10:19:04 +00:00
# get the actor inbox for the To handle
2020-04-04 10:05:27 +00:00
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
fromNickname,
fromDomain, postToBox)
2019-07-16 10:19:04 +00:00
if not inboxUrl:
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: No ' + postToBox + ' was found for ' + handle)
2019-07-16 10:19:04 +00:00
return 3
if not fromPersonId:
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: No actor was found for ' + handle)
2019-07-16 10:19:04 +00:00
return 4
# Get the json for the c2s post, not saving anything to file
# Note that baseDir is set to None
2020-04-04 10:05:27 +00:00
saveToFile = False
clientToServer = True
2019-07-17 14:43:51 +00:00
if toDomain.lower().endswith('public'):
2020-04-04 10:05:27 +00:00
toPersonId = 'https://www.w3.org/ns/activitystreams#Public'
fromDomainFull = fromDomain
2019-07-17 14:43:51 +00:00
if fromPort:
2020-04-04 10:05:27 +00:00
if fromPort != 80 and fromPort != 443:
if ':' not in fromDomain:
2020-04-04 10:05:27 +00:00
fromDomainFull = fromDomain + ':' + str(fromPort)
cc = httpPrefix + '://' + fromDomainFull + '/users/' + \
fromNickname + '/followers'
2019-07-17 14:43:51 +00:00
else:
if toDomain.lower().endswith('followers') or \
toDomain.lower().endswith('followersonly'):
2020-04-04 10:05:27 +00:00
toPersonId = \
httpPrefix + '://' + \
fromDomainFull + '/users/' + fromNickname + '/followers'
2019-07-17 14:43:51 +00:00
else:
2020-04-04 10:05:27 +00:00
toDomainFull = toDomain
if toPort:
2020-04-04 10:05:27 +00:00
if toPort != 80 and toPort != 443:
if ':' not in toDomain:
2020-04-04 10:05:27 +00:00
toDomainFull = toDomain + ':' + str(toPort)
toPersonId = httpPrefix + '://' + toDomainFull + \
'/users/' + toNickname
postJsonObject = \
createPostBase(baseDir,
fromNickname, fromDomain, fromPort,
toPersonId, cc, httpPrefix, content,
followersOnly, saveToFile, clientToServer,
attachImageFilename, mediaType,
imageDescription, useBlurhash,
False, isArticle, inReplyTo,
inReplyToAtomUri, subject,
False, None, None, None)
authHeader = createBasicAuthHeader(fromNickname, password)
2019-07-16 14:23:06 +00:00
if attachImageFilename:
2020-04-04 10:05:27 +00:00
headers = {
'host': fromDomain,
2020-03-22 20:36:19 +00:00
'Authorization': authHeader
}
2020-04-04 10:05:27 +00:00
postResult = \
postImage(session, attachImageFilename, [],
inboxUrl, headers, "inbox:write")
if not postResult:
if debug:
print('DEBUG: Failed to upload image')
2020-04-04 10:08:37 +00:00
# return 9
2020-04-04 10:05:27 +00:00
headers = {
'host': fromDomain,
'Content-type': 'application/json',
2020-03-22 20:36:19 +00:00
'Authorization': authHeader
}
2020-04-04 10:05:27 +00:00
postResult = \
postJsonString(session, json.dumps(postJsonObject), [],
inboxUrl, headers, "inbox:write", debug)
if not postResult:
if debug:
print('DEBUG: POST failed for c2s to '+inboxUrl)
return 5
2019-07-16 10:19:04 +00:00
if debug:
print('DEBUG: c2s POST success')
return 0
2020-04-04 10:05:27 +00:00
def groupFollowersByDomain(baseDir: str, nickname: str, domain: str) -> {}:
"""Returns a dictionary with followers grouped by domain
"""
2020-04-04 10:05:27 +00:00
handle = nickname + '@' + domain
followersFilename = baseDir + '/accounts/' + handle + '/followers.txt'
if not os.path.isfile(followersFilename):
return None
2020-04-04 10:05:27 +00:00
grouped = {}
with open(followersFilename, "r") as f:
for followerHandle in f:
if '@' in followerHandle:
2020-05-22 11:32:38 +00:00
fHandle = \
followerHandle.strip().replace('\n', '').replace('\r', '')
2020-04-04 10:05:27 +00:00
followerDomain = fHandle.split('@')[1]
if not grouped.get(followerDomain):
2020-04-04 10:05:27 +00:00
grouped[followerDomain] = [fHandle]
else:
grouped[followerDomain].append(fHandle)
return grouped
2019-10-16 10:58:31 +00:00
2020-04-04 10:05:27 +00:00
2019-10-16 10:58:31 +00:00
def addFollowersToPublicPost(postJsonObject: {}) -> None:
"""Adds followers entry to cc if it doesn't exist
"""
if not postJsonObject.get('actor'):
return
if isinstance(postJsonObject['object'], str):
if not postJsonObject.get('to'):
return
2020-04-04 10:05:27 +00:00
if len(postJsonObject['to']) > 1:
2019-10-16 10:58:31 +00:00
return
2020-04-04 10:05:27 +00:00
if len(postJsonObject['to']) == 0:
2019-10-16 10:58:31 +00:00
return
if not postJsonObject['to'][0].endswith('#Public'):
return
if postJsonObject.get('cc'):
return
2020-04-04 10:05:27 +00:00
postJsonObject['cc'] = postJsonObject['actor'] + '/followers'
2019-10-16 10:58:31 +00:00
elif isinstance(postJsonObject['object'], dict):
if not postJsonObject['object'].get('to'):
return
2020-04-04 10:05:27 +00:00
if len(postJsonObject['object']['to']) > 1:
2019-10-16 10:58:31 +00:00
return
2020-04-04 10:05:27 +00:00
if len(postJsonObject['object']['to']) == 0:
2019-10-16 10:58:31 +00:00
return
if not postJsonObject['object']['to'][0].endswith('#Public'):
return
if postJsonObject['object'].get('cc'):
return
2020-04-04 10:05:27 +00:00
postJsonObject['object']['cc'] = postJsonObject['actor'] + '/followers'
def sendSignedJson(postJsonObject: {}, session, baseDir: str,
nickname: str, domain: str, port: int,
toNickname: str, toDomain: str, toPort: int, cc: str,
httpPrefix: str, saveToFile: bool, clientToServer: bool,
federationList: [],
sendThreads: [], postLog: [], cachedWebfingers: {},
personCache: {}, debug: bool, projectVersion: str) -> int:
2019-07-05 18:57:19 +00:00
"""Sends a signed json object to an inbox/outbox
"""
2019-07-16 22:57:45 +00:00
if debug:
print('DEBUG: sendSignedJson start')
2019-07-16 10:19:04 +00:00
if not session:
print('WARN: No session specified for sendSignedJson')
return 8
2020-04-04 10:05:27 +00:00
withDigest = True
2019-07-05 18:57:19 +00:00
2020-06-19 22:50:41 +00:00
if toDomain.endswith('.onion') or toDomain.endswith('.i2p'):
2020-04-04 10:05:27 +00:00
httpPrefix = 'http'
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
# sharedInbox = False
if toNickname == 'inbox':
2019-08-23 13:47:29 +00:00
# shared inbox actor on @domain@domain
2020-04-04 10:05:27 +00:00
toNickname = toDomain
# sharedInbox = True
2019-08-16 20:04:24 +00:00
if toPort:
2020-04-04 10:05:27 +00:00
if toPort != 80 and toPort != 443:
2019-08-16 20:04:24 +00:00
if ':' not in toDomain:
2020-04-04 10:05:27 +00:00
toDomain = toDomain + ':' + str(toPort)
2019-07-05 18:57:19 +00:00
toDomainUrl = httpPrefix + '://' + toDomain
if not siteIsActive(toDomainUrl):
print('Domain is inactive: ' + toDomainUrl)
return 9
2020-06-23 10:41:12 +00:00
print('Domain is active: ' + toDomainUrl)
handleBase = toDomainUrl + '/@'
2019-10-21 14:12:22 +00:00
if toNickname:
2020-04-04 10:05:27 +00:00
handle = handleBase + toNickname
2019-10-21 14:12:22 +00:00
else:
2020-04-04 10:05:27 +00:00
singleUserInstanceNickname = 'dev'
2020-05-17 12:16:40 +00:00
handle = handleBase + singleUserInstanceNickname
2020-03-22 21:16:02 +00:00
2019-07-16 22:57:45 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: handle - ' + handle + ' toPort ' + str(toPort))
2019-07-05 18:57:19 +00:00
2019-08-23 13:47:29 +00:00
# lookup the inbox for the To handle
2020-04-04 10:05:27 +00:00
wfRequest = webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
domain, projectVersion)
2019-08-23 13:47:29 +00:00
if not wfRequest:
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: webfinger for ' + handle + ' failed')
2019-08-23 13:47:29 +00:00
return 1
2020-06-23 10:41:12 +00:00
if not isinstance(wfRequest, dict):
print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
return 1
2019-07-05 18:57:19 +00:00
2019-10-17 14:41:47 +00:00
if wfRequest.get('errors'):
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: webfinger for ' + handle +
' failed with errors ' + str(wfRequest['errors']))
2020-03-22 21:16:02 +00:00
2019-07-05 22:13:20 +00:00
if not clientToServer:
2020-04-04 10:05:27 +00:00
postToBox = 'inbox'
2019-07-05 22:13:20 +00:00
else:
2020-04-04 10:05:27 +00:00
postToBox = 'outbox'
2019-07-05 22:13:20 +00:00
# get the actor inbox/outbox/capabilities for the To handle
2020-05-17 12:16:40 +00:00
(inboxUrl, pubKeyId, pubKey, toPersonId, sharedInboxUrl,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain, postToBox)
2020-04-04 10:05:27 +00:00
if nickname == 'capabilities':
inboxUrl = capabilityAcquisition
2019-07-05 22:13:20 +00:00
if not capabilityAcquisition:
return 2
else:
2020-04-04 10:05:27 +00:00
print("inboxUrl: " + str(inboxUrl))
print("toPersonId: " + str(toPersonId))
print("sharedInboxUrl: " + str(sharedInboxUrl))
2019-09-16 13:06:38 +00:00
if inboxUrl:
if inboxUrl.endswith('/actor/inbox'):
2020-04-04 10:05:27 +00:00
inboxUrl = sharedInboxUrl
2019-07-06 13:49:25 +00:00
2019-07-05 18:57:19 +00:00
if not inboxUrl:
2019-07-16 22:57:45 +00:00
if debug:
print('DEBUG: missing inboxUrl')
2019-07-05 18:57:19 +00:00
return 3
2019-08-04 21:26:31 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: Sending to endpoint ' + inboxUrl)
2020-03-22 21:16:02 +00:00
2019-07-05 22:13:20 +00:00
if not pubKey:
2019-07-16 22:57:45 +00:00
if debug:
print('DEBUG: missing pubkey')
2019-07-05 18:57:19 +00:00
return 4
2019-07-05 22:13:20 +00:00
if not toPersonId:
2019-07-16 22:57:45 +00:00
if debug:
print('DEBUG: missing personId')
2019-07-05 22:13:20 +00:00
return 5
# sharedInbox and capabilities are optional
2019-07-05 18:57:19 +00:00
# get the senders private key
2020-04-04 10:05:27 +00:00
privateKeyPem = getPersonKey(nickname, domain, baseDir, 'private', debug)
if len(privateKeyPem) == 0:
2019-07-06 13:49:25 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: Private key not found for ' +
nickname + '@' + domain + ' in ' + baseDir + '/keys/private')
2019-07-05 22:13:20 +00:00
return 6
2019-07-05 18:57:19 +00:00
2019-07-05 22:13:20 +00:00
if toDomain not in inboxUrl:
2019-07-16 22:57:45 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: ' + toDomain + ' is not in ' + inboxUrl)
2019-07-05 22:13:20 +00:00
return 7
2020-04-04 10:05:27 +00:00
postPath = inboxUrl.split(toDomain, 1)[1]
2019-10-16 10:58:31 +00:00
addFollowersToPublicPost(postJsonObject)
2020-03-22 21:16:02 +00:00
if not postJsonObject.get('signature'):
try:
signedPostJsonObject = jsonldSign(postJsonObject, privateKeyPem)
postJsonObject = signedPostJsonObject
except BaseException:
print('WARN: failed to JSON-LD sign post')
pass
2020-06-15 13:08:19 +00:00
# convert json to string so that there are no
# subsequent conversions after creating message body digest
2020-04-04 10:05:27 +00:00
postJsonStr = json.dumps(postJsonObject)
# construct the http header, including the message body digest
2020-04-04 10:05:27 +00:00
signatureHeaderJson = \
createSignedHeader(privateKeyPem, nickname, domain, port,
toDomain, toPort,
postPath, httpPrefix, withDigest, postJsonStr)
2020-03-22 21:16:02 +00:00
2019-06-30 13:20:23 +00:00
# Keep the number of threads being used small
2020-04-04 10:05:27 +00:00
while len(sendThreads) > 1000:
2019-10-04 12:22:56 +00:00
print('WARN: Maximum threads reached - killing send thread')
2019-06-30 15:03:26 +00:00
sendThreads[0].kill()
2019-06-30 13:38:01 +00:00
sendThreads.pop(0)
2019-10-04 12:22:56 +00:00
print('WARN: thread killed')
2019-10-16 18:19:18 +00:00
2019-07-16 22:57:45 +00:00
if debug:
print('DEBUG: starting thread to send post')
2019-07-18 11:35:48 +00:00
pprint(postJsonObject)
2020-04-04 10:05:27 +00:00
thr = \
threadWithTrace(target=threadSendPost,
args=(session,
postJsonStr,
federationList,
inboxUrl, baseDir,
signatureHeaderJson.copy(),
2020-03-22 20:36:19 +00:00
postLog,
2020-04-04 10:05:27 +00:00
debug), daemon=True)
2019-06-30 13:20:23 +00:00
sendThreads.append(thr)
2020-04-04 10:05:27 +00:00
# thr.start()
2019-06-30 10:14:02 +00:00
return 0
2020-04-04 10:05:27 +00:00
def addToField(activityType: str, postJsonObject: {},
debug: bool) -> ({}, bool):
2019-08-18 09:39:12 +00:00
"""The Follow activity doesn't have a 'to' field and so one
needs to be added so that activity distribution happens in a consistent way
Returns true if a 'to' field exists or was added
"""
if postJsonObject.get('to'):
2020-04-04 10:05:27 +00:00
return postJsonObject, True
2020-03-22 21:16:02 +00:00
2019-08-18 09:39:12 +00:00
if debug:
pprint(postJsonObject)
print('DEBUG: no "to" field when sending to named addresses 2')
2020-04-04 10:05:27 +00:00
isSameType = False
toFieldAdded = False
2019-08-18 09:39:12 +00:00
if postJsonObject.get('object'):
if isinstance(postJsonObject['object'], str):
if postJsonObject.get('type'):
2020-04-04 10:05:27 +00:00
if postJsonObject['type'] == activityType:
isSameType = True
2019-08-18 09:39:12 +00:00
if debug:
print('DEBUG: "to" field assigned to Follow')
2020-04-04 10:05:27 +00:00
toAddress = postJsonObject['object']
2019-08-18 16:49:35 +00:00
if '/statuses/' in toAddress:
2020-04-04 10:05:27 +00:00
toAddress = toAddress.split('/statuses/')[0]
postJsonObject['to'] = [toAddress]
toFieldAdded = True
2019-08-18 09:39:12 +00:00
elif isinstance(postJsonObject['object'], dict):
if postJsonObject['object'].get('type'):
2020-04-04 10:05:27 +00:00
if postJsonObject['object']['type'] == activityType:
isSameType = True
2019-08-18 09:39:12 +00:00
if isinstance(postJsonObject['object']['object'], str):
if debug:
print('DEBUG: "to" field assigned to Follow')
2020-04-04 10:05:27 +00:00
toAddress = postJsonObject['object']['object']
2019-08-18 16:49:35 +00:00
if '/statuses/' in toAddress:
2020-04-04 10:05:27 +00:00
toAddress = toAddress.split('/statuses/')[0]
postJsonObject['object']['to'] = [toAddress]
postJsonObject['to'] = \
2019-12-12 09:58:06 +00:00
[postJsonObject['object']['object']]
2020-04-04 10:05:27 +00:00
toFieldAdded = True
2019-08-18 09:39:12 +00:00
if not isSameType:
2020-04-04 10:05:27 +00:00
return postJsonObject, True
2019-08-18 09:39:12 +00:00
if toFieldAdded:
2020-04-04 10:05:27 +00:00
return postJsonObject, True
return postJsonObject, False
def sendToNamedAddresses(session, baseDir: str,
nickname: str,
2020-06-03 20:21:44 +00:00
domain: str,
onionDomain: str, i2pDomain: str, port: int,
2020-04-04 10:05:27 +00:00
httpPrefix: str, federationList: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
postJsonObject: {}, debug: bool,
2019-08-14 20:12:27 +00:00
projectVersion: str) -> None:
2019-07-15 18:20:52 +00:00
"""sends a post to the specific named addresses in to/cc
"""
2019-07-16 10:19:04 +00:00
if not session:
print('WARN: No session for sendToNamedAddresses')
return
2019-07-15 18:20:52 +00:00
if not postJsonObject.get('object'):
2019-07-16 10:19:04 +00:00
return
2019-08-20 20:35:15 +00:00
if isinstance(postJsonObject['object'], dict):
2020-04-04 10:05:27 +00:00
isProfileUpdate = False
2019-08-20 20:35:15 +00:00
# for actor updates there is no 'to' within the object
if postJsonObject['object'].get('type') and postJsonObject.get('type'):
2020-04-04 10:05:27 +00:00
if (postJsonObject['type'] == 'Update' and
(postJsonObject['object']['type'] == 'Person' or
postJsonObject['object']['type'] == 'Application' or
postJsonObject['object']['type'] == 'Group' or
postJsonObject['object']['type'] == 'Service')):
2019-08-20 20:35:15 +00:00
# use the original object, which has a 'to'
2020-04-04 10:05:27 +00:00
recipientsObject = postJsonObject
isProfileUpdate = True
2020-03-22 21:16:02 +00:00
2019-08-20 20:35:15 +00:00
if not isProfileUpdate:
2019-08-18 09:39:12 +00:00
if not postJsonObject['object'].get('to'):
2019-08-20 20:35:15 +00:00
if debug:
pprint(postJsonObject)
2020-04-04 10:05:27 +00:00
print('DEBUG: ' +
'no "to" field when sending to named addresses')
2020-03-22 21:16:02 +00:00
if postJsonObject['object'].get('type'):
2020-04-04 10:05:27 +00:00
if postJsonObject['object']['type'] == 'Follow':
2019-08-20 20:35:15 +00:00
if isinstance(postJsonObject['object']['object'], str):
if debug:
print('DEBUG: "to" field assigned to Follow')
2020-04-04 10:05:27 +00:00
postJsonObject['object']['to'] = \
2019-12-12 09:58:06 +00:00
[postJsonObject['object']['object']]
2019-08-20 20:35:15 +00:00
if not postJsonObject['object'].get('to'):
return
2020-04-04 10:05:27 +00:00
recipientsObject = postJsonObject['object']
2020-03-22 21:16:02 +00:00
else:
2020-04-04 10:05:27 +00:00
postJsonObject, fieldAdded = \
addToField('Follow', postJsonObject, debug)
2019-08-18 16:49:35 +00:00
if not fieldAdded:
return
2020-04-04 10:05:27 +00:00
postJsonObject, fieldAdded = addToField('Like', postJsonObject, debug)
2019-08-18 09:39:12 +00:00
if not fieldAdded:
2019-07-16 19:07:45 +00:00
return
2020-04-04 10:05:27 +00:00
recipientsObject = postJsonObject
2019-07-15 18:20:52 +00:00
2020-04-04 10:05:27 +00:00
recipients = []
recipientType = ('to', 'cc')
2019-07-15 18:20:52 +00:00
for rType in recipientType:
2019-08-18 09:39:12 +00:00
if not recipientsObject.get(rType):
continue
2019-08-18 20:54:33 +00:00
if isinstance(recipientsObject[rType], list):
2019-08-18 21:08:38 +00:00
if debug:
2019-08-19 08:58:04 +00:00
pprint(recipientsObject)
2020-04-04 10:05:27 +00:00
print('recipientsObject: ' + str(recipientsObject[rType]))
2019-08-18 20:54:33 +00:00
for address in recipientsObject[rType]:
2019-08-18 21:15:09 +00:00
if not address:
continue
if '/' not in address:
continue
2019-08-18 20:54:33 +00:00
if address.endswith('#Public'):
continue
if address.endswith('/followers'):
continue
recipients.append(address)
elif isinstance(recipientsObject[rType], str):
2020-04-04 10:05:27 +00:00
address = recipientsObject[rType]
2019-08-18 21:15:09 +00:00
if address:
if '/' in address:
if address.endswith('#Public'):
continue
if address.endswith('/followers'):
continue
recipients.append(address)
2019-07-15 18:20:52 +00:00
if not recipients:
2019-08-18 20:54:33 +00:00
if debug:
print('DEBUG: no individual recipients')
2019-07-15 18:20:52 +00:00
return
2019-07-15 18:29:30 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: Sending individually addressed posts: ' +
str(recipients))
2019-07-15 18:29:30 +00:00
# this is after the message has arrived at the server
2020-04-04 10:05:27 +00:00
clientToServer = False
2019-07-15 18:20:52 +00:00
for address in recipients:
2020-04-04 10:05:27 +00:00
toNickname = getNicknameFromActor(address)
2019-07-15 18:20:52 +00:00
if not toNickname:
continue
2020-04-04 10:05:27 +00:00
toDomain, toPort = getDomainFromActor(address)
2019-07-15 18:20:52 +00:00
if not toDomain:
continue
2019-07-15 18:29:30 +00:00
if debug:
2020-04-04 10:05:27 +00:00
domainFull = domain
2019-07-16 10:19:04 +00:00
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
toDomainFull = toDomain
2019-07-16 10:19:04 +00:00
if toPort:
2020-04-04 10:05:27 +00:00
if toPort != 80 and toPort != 443:
if ':' not in toDomain:
2020-04-04 10:05:27 +00:00
toDomainFull = toDomain + ':' + str(toPort)
print('DEBUG: Post sending s2s: ' + nickname + '@' + domainFull +
' to ' + toNickname + '@' + toDomainFull)
2020-03-02 16:11:34 +00:00
# if we have an alt onion domain and we are sending to
# another onion domain then switch the clearnet
# domain for the onion one
2020-04-04 10:05:27 +00:00
fromDomain = domain
fromHttpPrefix = httpPrefix
2020-03-02 16:11:34 +00:00
if onionDomain:
if toDomain.endswith('.onion'):
2020-04-04 10:05:27 +00:00
fromDomain = onionDomain
fromHttpPrefix = 'http'
2020-06-03 20:21:44 +00:00
elif i2pDomain:
if toDomain.endswith('.i2p'):
fromDomain = i2pDomain
2020-06-19 22:50:41 +00:00
fromHttpPrefix = 'http'
2020-04-04 10:05:27 +00:00
cc = []
sendSignedJson(postJsonObject, session, baseDir,
nickname, fromDomain, port,
toNickname, toDomain, toPort,
cc, fromHttpPrefix, True, clientToServer,
federationList,
sendThreads, postLog, cachedWebfingers,
personCache, debug, projectVersion)
def hasSharedInbox(session, httpPrefix: str, domain: str) -> bool:
"""Returns true if the given domain has a shared inbox
"""
2020-04-04 10:05:27 +00:00
wfRequest = webfingerHandle(session, domain + '@' + domain,
httpPrefix, {},
None, __version__)
if wfRequest:
2020-06-23 10:41:12 +00:00
if isinstance(wfRequest, dict):
if not wfRequest.get('errors'):
return True
return False
2019-11-04 10:43:19 +00:00
2020-04-04 10:05:27 +00:00
def sendToFollowers(session, baseDir: str,
nickname: str,
2020-06-03 20:21:44 +00:00
domain: str,
onionDomain: str, i2pDomain: str, port: int,
2020-04-04 10:05:27 +00:00
httpPrefix: str, federationList: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
postJsonObject: {}, debug: bool,
2019-08-14 20:12:27 +00:00
projectVersion: str) -> None:
2019-07-08 13:30:04 +00:00
"""sends a post to the followers of the given nickname
"""
2019-08-20 21:04:24 +00:00
print('sendToFollowers')
2019-07-16 10:19:04 +00:00
if not session:
print('WARN: No session for sendToFollowers')
return
2020-04-04 10:05:27 +00:00
if not postIsAddressedToFollowers(baseDir, nickname, domain,
port, httpPrefix,
postJsonObject):
2019-07-15 18:29:30 +00:00
if debug:
print('Post is not addressed to followers')
2019-07-08 13:30:04 +00:00
return
2019-08-20 21:04:24 +00:00
print('Post is addressed to followers')
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
grouped = groupFollowersByDomain(baseDir, nickname, domain)
2019-07-08 13:30:04 +00:00
if not grouped:
2019-07-15 18:29:30 +00:00
if debug:
print('Post to followers did not resolve any domains')
2019-07-08 13:30:04 +00:00
return
2019-08-20 21:04:24 +00:00
print('Post to followers resolved domains')
2019-08-26 13:34:41 +00:00
print(str(grouped))
2019-07-08 13:30:04 +00:00
2019-07-15 18:29:30 +00:00
# this is after the message has arrived at the server
2020-04-04 10:05:27 +00:00
clientToServer = False
2019-07-15 18:20:52 +00:00
2019-07-08 13:30:04 +00:00
# for each instance
2020-04-04 10:05:27 +00:00
for followerDomain, followerHandles in grouped.items():
2019-07-16 22:57:45 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: follower handles for ' + followerDomain)
2019-07-16 22:57:45 +00:00
pprint(followerHandles)
# check that the follower's domain is active
followerDomainUrl = httpPrefix + '://' + followerDomain
if not siteIsActive(followerDomainUrl):
print('Domain is inactive: ' + followerDomainUrl)
continue
print('Domain is active: ' + followerDomainUrl)
2020-04-04 10:05:27 +00:00
withSharedInbox = hasSharedInbox(session, httpPrefix, followerDomain)
if debug:
2019-08-26 17:44:21 +00:00
if withSharedInbox:
2020-04-04 10:05:27 +00:00
print(followerDomain + ' has shared inbox')
else:
2020-04-04 10:05:27 +00:00
print(followerDomain + ' does not have a shared inbox')
2020-04-04 10:05:27 +00:00
toPort = port
index = 0
toDomain = followerHandles[index].split('@')[1]
2019-07-08 13:30:04 +00:00
if ':' in toDomain:
2020-04-04 10:05:27 +00:00
toPort = toDomain.split(':')[1]
toDomain = toDomain.split(':')[0]
2019-08-22 19:47:10 +00:00
2020-04-04 10:05:27 +00:00
cc = ''
2019-11-07 20:51:29 +00:00
2020-03-02 16:23:30 +00:00
# if we are sending to an onion domain and we
# have an alt onion domain then use the alt
2020-04-04 10:05:27 +00:00
fromDomain = domain
fromHttpPrefix = httpPrefix
2020-03-02 16:23:30 +00:00
if onionDomain:
2020-03-02 19:31:41 +00:00
if toDomain.endswith('.onion'):
2020-04-04 10:05:27 +00:00
fromDomain = onionDomain
fromHttpPrefix = 'http'
2020-06-03 20:21:44 +00:00
elif i2pDomain:
if toDomain.endswith('.i2p'):
fromDomain = i2pDomain
2020-06-19 22:50:41 +00:00
fromHttpPrefix = 'http'
2020-03-02 16:23:30 +00:00
2019-11-07 21:12:53 +00:00
if withSharedInbox:
2020-04-04 10:05:27 +00:00
toNickname = followerHandles[index].split('@')[0]
# if there are more than one followers on the domain
# then send the post to the shared inbox
2020-04-04 10:05:27 +00:00
if len(followerHandles) > 1:
toNickname = 'inbox'
2020-04-04 10:05:27 +00:00
if toNickname != 'inbox' and postJsonObject.get('type'):
if postJsonObject['type'] == 'Update':
2019-11-07 21:12:53 +00:00
if postJsonObject.get('object'):
if isinstance(postJsonObject['object'], dict):
if postJsonObject['object'].get('type'):
2020-04-04 10:05:27 +00:00
typ = postJsonObject['object']['type']
if typ == 'Person' or \
typ == 'Application' or \
typ == 'Group' or \
typ == 'Service':
print('Sending profile update to ' +
'shared inbox of ' + toDomain)
toNickname = 'inbox'
2020-02-05 11:46:05 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: Sending from ' + nickname + '@' + domain +
' to ' + toNickname + '@' + toDomain)
sendSignedJson(postJsonObject, session, baseDir,
nickname, fromDomain, port,
toNickname, toDomain, toPort,
cc, fromHttpPrefix, True, clientToServer,
federationList,
sendThreads, postLog, cachedWebfingers,
personCache, debug, projectVersion)
else:
# send to individual followers without using a shared inbox
for handle in followerHandles:
2019-11-07 19:23:56 +00:00
if debug:
2020-04-04 10:05:27 +00:00
print('DEBUG: Sending to ' + handle)
toNickname = handle.split('@')[0]
2020-03-22 21:16:02 +00:00
2019-11-07 12:56:00 +00:00
if debug:
2020-04-04 10:05:27 +00:00
if postJsonObject['type'] != 'Update':
print('DEBUG: Sending from ' +
nickname + '@' + domain + ' to ' +
toNickname + '@' + toDomain)
2019-11-07 12:56:00 +00:00
else:
2020-04-04 10:05:27 +00:00
print('DEBUG: Sending profile update from ' +
nickname + '@' + domain + ' to ' +
toNickname + '@' + toDomain)
sendSignedJson(postJsonObject, session, baseDir,
nickname, fromDomain, port,
toNickname, toDomain, toPort,
cc, fromHttpPrefix, True, clientToServer,
federationList,
sendThreads, postLog, cachedWebfingers,
personCache, debug, projectVersion)
2020-02-05 11:46:05 +00:00
2019-11-07 20:51:29 +00:00
time.sleep(4)
2019-11-04 10:43:19 +00:00
2019-11-07 21:16:40 +00:00
if debug:
print('DEBUG: End of sendToFollowers')
2020-04-04 10:05:27 +00:00
def sendToFollowersThread(session, baseDir: str,
nickname: str,
2020-06-03 20:21:44 +00:00
domain: str,
onionDomain: str, i2pDomain: str, port: int,
2020-04-04 10:05:27 +00:00
httpPrefix: str, federationList: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
postJsonObject: {}, debug: bool,
2019-11-04 10:43:19 +00:00
projectVersion: str):
"""Returns a thread used to send a post to followers
"""
2020-04-04 10:05:27 +00:00
sendThread = \
threadWithTrace(target=sendToFollowers,
args=(session, baseDir,
2020-06-03 20:21:44 +00:00
nickname, domain,
onionDomain, i2pDomain, port,
2020-04-04 10:05:27 +00:00
httpPrefix, federationList,
sendThreads, postLog,
cachedWebfingers, personCache,
postJsonObject.copy(), debug,
projectVersion), daemon=True)
try:
sendThread.start()
except SocketError as e:
print('WARN: socket error while starting ' +
'thread to send to followers. ' + str(e))
return None
2020-06-23 21:39:19 +00:00
except ValueError as e:
print('WARN: error while starting ' +
'thread to send to followers. ' + str(e))
return None
2019-11-04 10:43:19 +00:00
return sendThread
2019-07-08 13:30:04 +00:00
2020-04-04 10:05:27 +00:00
def createInbox(recentPostsCache: {},
session, baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
ocapAlways: bool, pageNumber=None) -> {}:
return createBoxIndexed(recentPostsCache,
session, baseDir, 'inbox',
nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
def createBookmarksTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlbookmarks',
nickname, domain,
port, httpPrefix, itemsPerPage, headerOnly,
True, ocapAlways, pageNumber)
def createDMTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'dm', nickname,
domain, port, httpPrefix, itemsPerPage,
headerOnly, True, ocapAlways, pageNumber)
def createRepliesTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlreplies',
nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
def createBlogsTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlblogs', nickname,
domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
def createMediaTimeline(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str, itemsPerPage: int,
headerOnly: bool, ocapAlways: bool,
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'tlmedia', nickname,
domain, port, httpPrefix,
itemsPerPage, headerOnly, True,
ocapAlways, pageNumber)
def createOutbox(session, baseDir: str, nickname: str, domain: str,
port: int, httpPrefix: str,
itemsPerPage: int, headerOnly: bool, authorized: bool,
pageNumber=None) -> {}:
return createBoxIndexed({}, session, baseDir, 'outbox',
nickname, domain, port, httpPrefix,
itemsPerPage, headerOnly, authorized,
False, pageNumber)
def createModeration(baseDir: str, nickname: str, domain: str, port: int,
httpPrefix: str, itemsPerPage: int, headerOnly: bool,
ocapAlways: bool, pageNumber=None) -> {}:
boxDir = createPersonDir(nickname, domain, baseDir, 'inbox')
boxname = 'moderation'
2019-08-12 13:22:17 +00:00
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domain = domain + ':' + str(port)
2019-08-12 13:22:17 +00:00
if not pageNumber:
2020-04-04 10:05:27 +00:00
pageNumber = 1
2019-11-16 22:09:54 +00:00
2020-04-04 10:05:27 +00:00
pageStr = '?page=' + str(pageNumber)
boxUrl = httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname
boxHeader = {
2020-03-22 20:36:19 +00:00
'@context': 'https://www.w3.org/ns/activitystreams',
2020-04-04 10:05:27 +00:00
'first': boxUrl+'?page=true',
'id': boxUrl,
'last': boxUrl+'?page=true',
2020-03-22 20:36:19 +00:00
'totalItems': 0,
'type': 'OrderedCollection'
}
2020-04-04 10:05:27 +00:00
boxItems = {
2020-03-22 20:36:19 +00:00
'@context': 'https://www.w3.org/ns/activitystreams',
2020-04-04 10:05:27 +00:00
'id': boxUrl+pageStr,
2020-03-22 20:36:19 +00:00
'orderedItems': [
],
2020-04-04 10:05:27 +00:00
'partOf': boxUrl,
2020-03-22 20:36:19 +00:00
'type': 'OrderedCollectionPage'
}
2019-08-12 13:22:17 +00:00
2020-04-04 10:05:27 +00:00
if isModerator(baseDir, nickname):
moderationIndexFile = baseDir + '/accounts/moderation.txt'
2019-08-12 13:22:17 +00:00
if os.path.isfile(moderationIndexFile):
with open(moderationIndexFile, "r") as f:
2020-04-04 10:05:27 +00:00
lines = f.readlines()
boxHeader['totalItems'] = len(lines)
2019-08-12 13:22:17 +00:00
if headerOnly:
return boxHeader
2020-04-04 10:05:27 +00:00
pageLines = []
if len(lines) > 0:
endLineNumber = len(lines) - 1 - int(itemsPerPage * pageNumber)
if endLineNumber < 0:
endLineNumber = 0
startLineNumber = \
len(lines) - 1 - int(itemsPerPage * (pageNumber - 1))
if startLineNumber < 0:
startLineNumber = 0
lineNumber = startLineNumber
while lineNumber >= endLineNumber:
2020-05-22 11:32:38 +00:00
pageLines.append(lines[lineNumber].strip('\n').strip('\r'))
2020-04-04 10:05:27 +00:00
lineNumber -= 1
2019-11-16 22:09:54 +00:00
2019-08-12 13:22:17 +00:00
for postUrl in pageLines:
2020-04-04 10:05:27 +00:00
postFilename = \
boxDir + '/' + postUrl.replace('/', '#') + '.json'
2019-08-12 13:22:17 +00:00
if os.path.isfile(postFilename):
2020-04-04 10:05:27 +00:00
postJsonObject = loadJson(postFilename)
2019-10-22 11:55:06 +00:00
if postJsonObject:
boxItems['orderedItems'].append(postJsonObject)
2019-08-12 13:22:17 +00:00
if headerOnly:
return boxHeader
return boxItems
2020-04-04 10:05:27 +00:00
def getStatusNumberFromPostFilename(filename) -> int:
"""Gets the status number from a post filename
2020-04-04 10:05:27 +00:00
eg. https:##testdomain.com:8085#users#testuser567#
statuses#1562958506952068.json
returns 156295850695206
"""
if '#statuses#' not in filename:
return None
2020-04-04 10:05:27 +00:00
return int(filename.split('#')[-1].replace('.json', ''))
2019-08-25 16:09:56 +00:00
def isDM(postJsonObject: {}) -> bool:
"""Returns true if the given post is a DM
"""
2020-04-04 10:05:27 +00:00
if postJsonObject['type'] != 'Create':
2019-08-25 16:09:56 +00:00
return False
if not postJsonObject.get('object'):
return False
if not isinstance(postJsonObject['object'], dict):
return False
2020-04-04 10:05:27 +00:00
if postJsonObject['object']['type'] != 'Note' and \
2020-05-03 12:52:13 +00:00
postJsonObject['object']['type'] != 'Patch' and \
2020-04-04 10:05:27 +00:00
postJsonObject['object']['type'] != 'Article':
2019-08-25 16:09:56 +00:00
return False
2019-11-16 17:29:02 +00:00
if postJsonObject['object'].get('moderationStatus'):
return False
2020-04-04 10:05:27 +00:00
fields = ('to', 'cc')
2020-03-22 21:16:02 +00:00
for f in fields:
2019-08-25 16:09:56 +00:00
if not postJsonObject['object'].get(f):
continue
for toAddress in postJsonObject['object'][f]:
if toAddress.endswith('#Public'):
return False
if toAddress.endswith('followers'):
return False
return True
2020-04-04 10:05:27 +00:00
def isImageMedia(session, baseDir: str, httpPrefix: str,
nickname: str, domain: str,
2020-06-12 11:50:49 +00:00
postJsonObject: {}, translate: {}) -> bool:
2019-09-28 11:29:42 +00:00
"""Returns true if the given post has attached image media
"""
2020-04-04 10:05:27 +00:00
if postJsonObject['type'] == 'Announce':
postJsonAnnounce = \
downloadAnnounce(session, baseDir, httpPrefix,
nickname, domain, postJsonObject,
2020-06-12 11:50:49 +00:00
__version__, translate)
2019-09-28 16:21:43 +00:00
if postJsonAnnounce:
2020-04-04 10:05:27 +00:00
postJsonObject = postJsonAnnounce
if postJsonObject['type'] != 'Create':
2019-09-28 11:29:42 +00:00
return False
if not postJsonObject.get('object'):
return False
if not isinstance(postJsonObject['object'], dict):
return False
2019-11-16 22:20:16 +00:00
if postJsonObject['object'].get('moderationStatus'):
return False
2020-04-04 10:05:27 +00:00
if postJsonObject['object']['type'] != 'Note' and \
postJsonObject['object']['type'] != 'Article':
2019-09-28 11:29:42 +00:00
return False
if not postJsonObject['object'].get('attachment'):
return False
if not isinstance(postJsonObject['object']['attachment'], list):
return False
for attach in postJsonObject['object']['attachment']:
if attach.get('mediaType') and attach.get('url'):
if attach['mediaType'].startswith('image/') or \
attach['mediaType'].startswith('audio/') or \
attach['mediaType'].startswith('video/'):
2019-09-28 11:29:42 +00:00
return True
return False
2020-04-04 10:05:27 +00:00
def isReply(postJsonObject: {}, actor: str) -> bool:
2019-09-23 19:53:18 +00:00
"""Returns true if the given post is a reply to the given actor
"""
2020-04-04 10:05:27 +00:00
if postJsonObject['type'] != 'Create':
2019-11-09 21:39:04 +00:00
return False
if not postJsonObject.get('object'):
return False
if not isinstance(postJsonObject['object'], dict):
return False
2019-11-16 22:18:20 +00:00
if postJsonObject['object'].get('moderationStatus'):
return False
2020-04-04 10:05:27 +00:00
if postJsonObject['object']['type'] != 'Note' and \
postJsonObject['object']['type'] != 'Article':
2019-11-09 21:39:04 +00:00
return False
if postJsonObject['object'].get('inReplyTo'):
if postJsonObject['object']['inReplyTo'].startswith(actor):
2020-03-22 21:16:02 +00:00
return True
2019-11-09 21:39:04 +00:00
if not postJsonObject['object'].get('tag'):
return False
if not isinstance(postJsonObject['object']['tag'], list):
return False
for tag in postJsonObject['object']['tag']:
if not tag.get('type'):
continue
2020-04-04 10:05:27 +00:00
if tag['type'] == 'Mention':
2019-11-09 21:39:04 +00:00
if not tag.get('href'):
2019-10-22 19:07:23 +00:00
continue
2019-11-09 21:39:04 +00:00
if actor in tag['href']:
return True
2019-10-22 19:07:23 +00:00
return False
2019-09-23 19:53:18 +00:00
2020-04-04 10:05:27 +00:00
def createBoxIndex(boxDir: str, postsInBoxDict: {}) -> int:
2019-10-20 09:22:40 +00:00
""" Creates an index for the given box
"""
2020-04-04 10:05:27 +00:00
postsCtr = 0
postsInPersonInbox = os.scandir(boxDir)
2019-10-20 09:22:40 +00:00
for postFilename in postsInPersonInbox:
2020-04-04 10:05:27 +00:00
postFilename = postFilename.name
2019-10-20 09:22:40 +00:00
if not postFilename.endswith('.json'):
continue
# extract the status number
2020-04-04 10:05:27 +00:00
statusNumber = getStatusNumberFromPostFilename(postFilename)
2019-10-20 09:22:40 +00:00
if statusNumber:
2020-04-04 10:05:27 +00:00
postsInBoxDict[statusNumber] = os.path.join(boxDir, postFilename)
postsCtr += 1
2019-10-20 09:22:40 +00:00
return postsCtr
2020-04-04 10:05:27 +00:00
def createSharedInboxIndex(baseDir: str, sharedBoxDir: str,
postsInBoxDict: {}, postsCtr: int,
nickname: str, domain: str,
2019-10-20 09:49:26 +00:00
ocapAlways: bool) -> int:
2019-10-20 09:36:07 +00:00
""" Creates an index for the given shared inbox
"""
2020-04-04 10:05:27 +00:00
handle = nickname + '@' + domain
followingFilename = baseDir + '/accounts/' + handle + '/following.txt'
postsInSharedInbox = os.scandir(sharedBoxDir)
followingHandles = None
2020-03-22 21:16:02 +00:00
for postFilename in postsInSharedInbox:
2020-04-04 10:05:27 +00:00
postFilename = postFilename.name
2019-10-20 09:36:07 +00:00
if not postFilename.endswith('.json'):
continue
2020-04-04 10:05:27 +00:00
statusNumber = getStatusNumberFromPostFilename(postFilename)
2019-10-20 09:36:07 +00:00
if not statusNumber:
continue
2020-03-22 21:16:02 +00:00
2020-04-04 10:05:27 +00:00
sharedInboxFilename = os.path.join(sharedBoxDir, postFilename)
2019-10-20 09:36:07 +00:00
# get the actor from the shared post
2020-04-04 10:05:27 +00:00
postJsonObject = loadJson(sharedInboxFilename, 0)
if not postJsonObject:
print('WARN: json load exception createSharedInboxIndex')
2019-10-20 09:36:07 +00:00
continue
2020-04-04 10:05:27 +00:00
actorNickname = getNicknameFromActor(postJsonObject['actor'])
2019-10-20 09:51:32 +00:00
if not actorNickname:
continue
2020-04-04 10:05:27 +00:00
actorDomain, actorPort = getDomainFromActor(postJsonObject['actor'])
2019-10-20 09:51:32 +00:00
if not actorDomain:
2019-10-20 09:36:07 +00:00
continue
2019-10-20 09:47:06 +00:00
2019-10-20 09:36:07 +00:00
# is the actor followed by this account?
2019-10-20 09:47:06 +00:00
if not followingHandles:
with open(followingFilename, 'r') as followingFile:
2020-04-04 10:05:27 +00:00
followingHandles = followingFile.read()
if actorNickname + '@' + actorDomain not in followingHandles:
2019-10-20 09:36:07 +00:00
continue
if ocapAlways:
2020-04-04 10:05:27 +00:00
capsList = None
2019-10-20 09:36:07 +00:00
# Note: should this be in the Create or the object of a post?
if postJsonObject.get('capability'):
2020-03-22 21:16:02 +00:00
if isinstance(postJsonObject['capability'], list):
2020-04-04 10:05:27 +00:00
capsList = postJsonObject['capability']
2019-10-20 09:36:07 +00:00
# Have capabilities been granted for the sender?
2020-04-04 10:05:27 +00:00
ocapFilename = \
baseDir + '/accounts/' + handle + '/ocap/granted/' + \
postJsonObject['actor'].replace('/', '#') + '.json'
2019-10-20 09:36:07 +00:00
if not os.path.isfile(ocapFilename):
continue
# read the capabilities id
2020-04-04 10:05:27 +00:00
ocapJson = loadJson(ocapFilename, 0)
if not ocapJson:
print('WARN: json load exception createSharedInboxIndex')
else:
2019-10-20 09:36:07 +00:00
if ocapJson.get('id'):
2020-03-22 21:16:02 +00:00
if ocapJson['id'] in capsList:
2020-04-04 10:05:27 +00:00
postsInBoxDict[statusNumber] = sharedInboxFilename
postsCtr += 1
2019-10-20 09:36:07 +00:00
else:
2020-04-04 10:05:27 +00:00
postsInBoxDict[statusNumber] = sharedInboxFilename
postsCtr += 1
2019-10-20 09:36:07 +00:00
return postsCtr
2020-04-04 10:05:27 +00:00
def addPostStringToTimeline(postStr: str, boxname: str,
postsInBox: [], boxActor: str) -> bool:
2019-11-18 11:28:17 +00:00
""" is this a valid timeline post?
"""
2020-05-03 13:18:35 +00:00
# must be a recognized ActivityPub type
2020-04-04 10:05:27 +00:00
if ('"Note"' in postStr or
'"Article"' in postStr or
2020-05-03 12:52:13 +00:00
'"Patch"' in postStr or
2020-04-04 10:05:27 +00:00
'"Announce"' in postStr or
('"Question"' in postStr and
('"Create"' in postStr or '"Update"' in postStr))):
2019-11-18 11:28:17 +00:00
2020-04-04 10:05:27 +00:00
if boxname == 'dm':
if '#Public' in postStr or '/followers' in postStr:
return False
2020-04-04 10:05:27 +00:00
elif boxname == 'tlreplies':
if boxActor not in postStr:
return False
2020-04-04 10:05:27 +00:00
elif boxname == 'tlblogs':
2020-02-24 14:39:25 +00:00
if '"Create"' not in postStr:
return False
if '"Article"' not in postStr:
return False
2020-04-04 10:05:27 +00:00
elif boxname == 'tlmedia':
if '"Create"' in postStr:
if 'mediaType' not in postStr or 'image/' not in postStr:
2019-11-18 11:28:17 +00:00
return False
# add the post to the dictionary
postsInBox.append(postStr)
return True
return False
2020-04-04 10:05:27 +00:00
def addPostToTimeline(filePath: str, boxname: str,
postsInBox: [], boxActor: str) -> bool:
""" Reads a post from file and decides whether it is valid
"""
with open(filePath, 'r') as postFile:
2020-04-04 10:05:27 +00:00
postStr = postFile.read()
return addPostStringToTimeline(postStr, boxname, postsInBox, boxActor)
2019-11-18 11:28:17 +00:00
return False
2020-04-04 10:05:27 +00:00
def createBoxIndexed(recentPostsCache: {},
session, baseDir: str, boxname: str,
nickname: str, domain: str, port: int, httpPrefix: str,
itemsPerPage: int, headerOnly: bool, authorized: bool,
ocapAlways: bool, pageNumber=None) -> {}:
2019-11-18 11:28:17 +00:00
"""Constructs the box feed for a person with the given nickname
"""
if not authorized or not pageNumber:
2020-04-04 10:05:27 +00:00
pageNumber = 1
2019-11-18 11:28:17 +00:00
2020-04-04 10:05:27 +00:00
if boxname != 'inbox' and boxname != 'dm' and \
boxname != 'tlreplies' and boxname != 'tlmedia' and \
boxname != 'tlblogs' and \
2020-05-21 20:48:51 +00:00
boxname != 'outbox' and boxname != 'tlbookmarks' and \
boxname != 'bookmarks':
2019-11-18 11:28:17 +00:00
return None
# bookmarks timeline is like the inbox but has its own separate index
2020-04-04 10:05:27 +00:00
indexBoxName = boxname
2020-05-21 22:02:27 +00:00
if boxname == "tlbookmarks":
boxname = "bookmarks"
indexBoxName = boxname
2019-11-18 11:28:17 +00:00
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
2019-11-18 11:28:17 +00:00
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domain = domain + ':' + str(port)
2019-11-18 11:28:17 +00:00
2020-04-04 10:05:27 +00:00
boxActor = httpPrefix + '://' + domain + '/users/' + nickname
2020-01-19 20:19:56 +00:00
2020-04-04 10:05:27 +00:00
pageStr = '?page=true'
2019-11-18 11:28:17 +00:00
if pageNumber:
2020-05-21 21:43:33 +00:00
if pageNumber < 1:
pageNumber = 1
2019-11-18 11:28:17 +00:00
try:
2020-04-04 10:05:27 +00:00
pageStr = '?page=' + str(pageNumber)
except BaseException:
2019-11-18 11:28:17 +00:00
pass
2020-04-04 10:05:27 +00:00
boxUrl = httpPrefix + '://' + domain + '/users/' + nickname + '/' + boxname
boxHeader = {
2020-03-22 20:36:19 +00:00
'@context': 'https://www.w3.org/ns/activitystreams',
2020-05-21 21:43:33 +00:00
'first': boxUrl + '?page=true',
2020-04-04 10:05:27 +00:00
'id': boxUrl,
2020-05-21 21:43:33 +00:00
'last': boxUrl + '?page=true',
2020-03-22 20:36:19 +00:00
'totalItems': 0,
'type': 'OrderedCollection'
}
2020-04-04 10:05:27 +00:00
boxItems = {
2020-03-22 20:36:19 +00:00
'@context': 'https://www.w3.org/ns/activitystreams',
2020-05-21 21:43:33 +00:00
'id': boxUrl + pageStr,
2020-03-22 20:36:19 +00:00
'orderedItems': [
],
2020-04-04 10:05:27 +00:00
'partOf': boxUrl,
2020-03-22 20:36:19 +00:00
'type': 'OrderedCollectionPage'
}
2019-11-18 11:28:17 +00:00
2020-04-04 10:05:27 +00:00
postsInBox = []
2019-11-18 11:28:17 +00:00
2020-04-04 10:05:27 +00:00
indexFilename = \
baseDir + '/accounts/' + nickname + '@' + domain + \
'/' + indexBoxName + '.index'
postsCtr = 0
2019-11-18 11:28:17 +00:00
if os.path.isfile(indexFilename):
2020-05-21 19:28:09 +00:00
maxPostCtr = itemsPerPage * pageNumber
2019-11-18 11:28:17 +00:00
with open(indexFilename, 'r') as indexFile:
2020-04-04 10:05:27 +00:00
while postsCtr < maxPostCtr:
postFilename = indexFile.readline()
2019-11-18 15:04:08 +00:00
if not postFilename:
2020-05-21 21:53:12 +00:00
break
2019-11-18 15:04:08 +00:00
# Skip through any posts previous to the current page
2020-04-04 10:05:27 +00:00
if postsCtr < int((pageNumber - 1) * itemsPerPage):
postsCtr += 1
2019-11-18 11:28:17 +00:00
continue
2019-11-18 12:54:41 +00:00
# if this is a full path then remove the directories
if '/' in postFilename:
2020-04-04 10:05:27 +00:00
postFilename = postFilename.split('/')[-1]
2019-11-18 12:54:41 +00:00
2019-11-18 11:28:17 +00:00
# filename of the post without any extension or path
2020-04-04 10:05:27 +00:00
# This should also correspond to any index entry in
# the posts cache
postUrl = \
2020-05-22 11:32:38 +00:00
postFilename.replace('\n', '').replace('\r', '')
postUrl = postUrl.replace('.json', '').strip()
2019-11-25 10:10:59 +00:00
# is the post cached in memory?
if recentPostsCache.get('index'):
if postUrl in recentPostsCache['index']:
if recentPostsCache['json'].get(postUrl):
2020-04-04 10:05:27 +00:00
url = recentPostsCache['json'][postUrl]
addPostStringToTimeline(url,
boxname, postsInBox,
boxActor)
2020-05-21 19:28:09 +00:00
postsCtr += 1
continue
# read the post from file
fullPostFilename = \
locatePost(baseDir, nickname,
domain, postUrl, False)
if fullPostFilename:
addPostToTimeline(fullPostFilename, boxname,
postsInBox, boxActor)
else:
print('WARN: unable to locate post ' + postUrl)
2020-04-04 10:05:27 +00:00
postsCtr += 1
2019-11-18 11:28:17 +00:00
# Generate first and last entries within header
2020-04-04 10:05:27 +00:00
if postsCtr > 0:
lastPage = int(postsCtr / itemsPerPage)
if lastPage < 1:
lastPage = 1
boxHeader['last'] = \
httpPrefix + '://' + domain + '/users/' + \
nickname + '/' + boxname + '?page=' + str(lastPage)
2019-11-18 11:28:17 +00:00
if headerOnly:
2020-04-04 10:05:27 +00:00
boxHeader['totalItems'] = len(postsInBox)
prevPageStr = 'true'
if pageNumber > 1:
prevPageStr = str(pageNumber - 1)
boxHeader['prev'] = \
httpPrefix + '://' + domain + '/users/' + \
nickname + '/' + boxname + '?page=' + prevPageStr
nextPageStr = str(pageNumber + 1)
boxHeader['next'] = \
httpPrefix + '://' + domain + '/users/' + \
nickname + '/' + boxname + '?page=' + nextPageStr
2019-11-18 11:28:17 +00:00
return boxHeader
2019-11-18 11:55:27 +00:00
for postStr in postsInBox:
2020-04-04 10:05:27 +00:00
p = None
2019-11-18 11:28:17 +00:00
try:
2020-04-04 10:05:27 +00:00
p = json.loads(postStr)
except BaseException:
2019-11-18 11:28:17 +00:00
continue
# remove any capability so that it's not displayed
if p.get('capability'):
del p['capability']
2020-04-04 10:05:27 +00:00
# Don't show likes, replies or shares (announces) to
# unauthorized viewers
2019-11-18 11:28:17 +00:00
if not authorized:
if p.get('object'):
2020-03-22 21:16:02 +00:00
if isinstance(p['object'], dict):
2019-11-18 11:28:17 +00:00
if p['object'].get('likes'):
2020-04-04 10:05:27 +00:00
p['likes'] = {'items': []}
2019-11-18 11:28:17 +00:00
if p['object'].get('replies'):
2020-04-04 10:05:27 +00:00
p['replies'] = {}
2019-11-18 11:28:17 +00:00
if p['object'].get('shares'):
2020-04-04 10:05:27 +00:00
p['shares'] = {}
2019-11-18 11:28:17 +00:00
if p['object'].get('bookmarks'):
2020-04-04 10:05:27 +00:00
p['bookmarks'] = {}
2019-11-18 11:28:17 +00:00
2019-11-18 12:02:55 +00:00
boxItems['orderedItems'].append(p)
2019-11-18 11:28:17 +00:00
return boxItems
2020-04-04 10:05:27 +00:00
def expireCache(baseDir: str, personCache: {},
httpPrefix: str, archiveDir: str,
recentPostsCache: {},
maxPostsInBox=32000):
2019-08-20 11:51:29 +00:00
"""Thread used to expire actors from the cache and archive old posts
"""
while True:
# once per day
2020-04-04 10:05:27 +00:00
time.sleep(60 * 60 * 24)
expirePersonCache(baseDir, personCache)
archivePosts(baseDir, httpPrefix, archiveDir, recentPostsCache,
maxPostsInBox)
2020-04-04 10:05:27 +00:00
2019-08-20 11:51:29 +00:00
2020-04-04 10:05:27 +00:00
def archivePosts(baseDir: str, httpPrefix: str, archiveDir: str,
recentPostsCache: {},
2019-12-12 09:58:06 +00:00
maxPostsInBox=32000) -> None:
2019-07-12 20:43:55 +00:00
"""Archives posts for all accounts
"""
if archiveDir:
if not os.path.isdir(archiveDir):
os.mkdir(archiveDir)
if archiveDir:
2020-04-04 10:05:27 +00:00
if not os.path.isdir(archiveDir + '/accounts'):
os.mkdir(archiveDir + '/accounts')
2019-07-12 20:43:55 +00:00
2020-04-04 10:05:27 +00:00
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
2019-07-12 20:43:55 +00:00
for handle in dirs:
if '@' in handle:
2020-04-04 10:05:27 +00:00
nickname = handle.split('@')[0]
domain = handle.split('@')[1]
archiveSubdir = None
2019-07-12 20:43:55 +00:00
if archiveDir:
2020-04-04 10:05:27 +00:00
if not os.path.isdir(archiveDir + '/accounts/' + handle):
os.mkdir(archiveDir + '/accounts/' + handle)
if not os.path.isdir(archiveDir + '/accounts/' +
handle + '/inbox'):
os.mkdir(archiveDir + '/accounts/' +
handle + '/inbox')
if not os.path.isdir(archiveDir + '/accounts/' +
handle + '/outbox'):
os.mkdir(archiveDir + '/accounts/' +
handle + '/outbox')
archiveSubdir = archiveDir + '/accounts/' + \
handle + '/inbox'
archivePostsForPerson(httpPrefix, nickname, domain, baseDir,
'inbox', archiveSubdir,
recentPostsCache, maxPostsInBox)
2019-07-12 20:43:55 +00:00
if archiveDir:
2020-04-04 10:05:27 +00:00
archiveSubdir = archiveDir + '/accounts/' + \
handle + '/outbox'
archivePostsForPerson(httpPrefix, nickname, domain, baseDir,
'outbox', archiveSubdir,
recentPostsCache, maxPostsInBox)
2019-07-12 20:43:55 +00:00
2020-04-04 10:05:27 +00:00
def archivePostsForPerson(httpPrefix: str, nickname: str, domain: str,
baseDir: str,
boxname: str, archiveDir: str,
recentPostsCache: {},
2020-04-04 10:05:27 +00:00
maxPostsInBox=32000) -> None:
2019-07-04 16:24:23 +00:00
"""Retain a maximum number of posts within the given box
2019-06-29 13:44:21 +00:00
Move any others to an archive directory
"""
2020-04-04 10:05:27 +00:00
if boxname != 'inbox' and boxname != 'outbox':
2019-07-04 16:24:23 +00:00
return
2019-07-12 20:43:55 +00:00
if archiveDir:
if not os.path.isdir(archiveDir):
2020-03-22 21:16:02 +00:00
os.mkdir(archiveDir)
2020-04-04 10:05:27 +00:00
boxDir = createPersonDir(nickname, domain, baseDir, boxname)
postsInBox = os.scandir(boxDir)
noOfPosts = 0
2019-10-19 10:19:19 +00:00
for f in postsInBox:
2020-04-04 10:05:27 +00:00
noOfPosts += 1
if noOfPosts <= maxPostsInBox:
print('Checked ' + str(noOfPosts) + ' ' + boxname +
' posts for ' + nickname + '@' + domain)
2019-06-29 13:44:21 +00:00
return
2019-10-20 11:18:25 +00:00
# remove entries from the index
2020-04-04 10:05:27 +00:00
handle = nickname + '@' + domain
indexFilename = baseDir + '/accounts/' + handle + '/' + boxname + '.index'
2019-10-20 11:18:25 +00:00
if os.path.isfile(indexFilename):
2020-04-04 10:05:27 +00:00
indexCtr = 0
2019-10-20 11:18:25 +00:00
# get the existing index entries as a string
2020-04-04 10:05:27 +00:00
newIndex = ''
2019-10-20 11:18:25 +00:00
with open(indexFilename, 'r') as indexFile:
for postId in indexFile:
2020-04-04 10:05:27 +00:00
newIndex += postId
indexCtr += 1
if indexCtr >= maxPostsInBox:
2019-10-20 11:18:25 +00:00
break
# save the new index file
2020-04-04 10:05:27 +00:00
if len(newIndex) > 0:
indexFile = open(indexFilename, 'w+')
2019-10-20 11:18:25 +00:00
if indexFile:
indexFile.write(newIndex)
indexFile.close()
2020-04-04 10:05:27 +00:00
postsInBoxDict = {}
postsCtr = 0
postsInBox = os.scandir(boxDir)
for postFilename in postsInBox:
2020-04-04 10:05:27 +00:00
postFilename = postFilename.name
if not postFilename.endswith('.json'):
continue
# Time of file creation
2020-04-04 10:05:27 +00:00
fullFilename = os.path.join(boxDir, postFilename)
2019-11-06 14:50:17 +00:00
if os.path.isfile(fullFilename):
2020-04-04 10:05:27 +00:00
content = open(fullFilename).read()
2019-11-06 14:50:17 +00:00
if '"published":' in content:
2020-04-04 10:05:27 +00:00
publishedStr = content.split('"published":')[1]
2019-11-06 14:50:17 +00:00
if '"' in publishedStr:
2020-04-04 10:05:27 +00:00
publishedStr = publishedStr.split('"')[1]
2019-11-06 14:54:17 +00:00
if publishedStr.endswith('Z'):
2020-04-04 10:05:27 +00:00
postsInBoxDict[publishedStr] = postFilename
postsCtr += 1
2020-04-04 10:05:27 +00:00
noOfPosts = postsCtr
if noOfPosts <= maxPostsInBox:
print('Checked ' + str(noOfPosts) + ' ' + boxname +
' posts for ' + nickname + '@' + domain)
return
2019-11-06 14:50:17 +00:00
# sort the list in ascending order of date
2020-04-04 10:05:27 +00:00
postsInBoxSorted = \
OrderedDict(sorted(postsInBoxDict.items(), reverse=False))
2019-09-14 17:12:03 +00:00
2019-10-19 10:10:52 +00:00
# directory containing cached html posts
2020-04-04 10:05:27 +00:00
postCacheDir = boxDir.replace('/' + boxname, '/postcache')
2019-10-19 10:10:52 +00:00
2020-04-04 10:05:27 +00:00
removeCtr = 0
for publishedStr, postFilename in postsInBoxSorted.items():
filePath = os.path.join(boxDir, postFilename)
2019-09-24 21:16:44 +00:00
if not os.path.isfile(filePath):
continue
if archiveDir:
2020-04-04 10:05:27 +00:00
repliesPath = filePath.replace('.json', '.replies')
archivePath = os.path.join(archiveDir, postFilename)
os.rename(filePath, archivePath)
2019-09-24 21:16:44 +00:00
if os.path.isfile(repliesPath):
2020-04-04 10:05:27 +00:00
os.rename(repliesPath, archivePath)
2019-09-24 21:16:44 +00:00
else:
deletePost(baseDir, httpPrefix, nickname, domain,
filePath, False, recentPostsCache)
2019-10-19 10:10:52 +00:00
# remove cached html posts
2020-04-04 10:05:27 +00:00
postCacheFilename = \
os.path.join(postCacheDir, postFilename).replace('.json', '.html')
2019-10-19 10:10:52 +00:00
if os.path.isfile(postCacheFilename):
os.remove(postCacheFilename)
2020-04-04 10:05:27 +00:00
noOfPosts -= 1
removeCtr += 1
if noOfPosts <= maxPostsInBox:
2019-09-24 21:16:44 +00:00
break
2020-02-26 20:39:18 +00:00
if archiveDir:
2020-04-04 10:05:27 +00:00
print('Archived ' + str(removeCtr) + ' ' + boxname +
' posts for ' + nickname + '@' + domain)
2020-02-26 20:39:18 +00:00
else:
2020-04-04 10:05:27 +00:00
print('Removed ' + str(removeCtr) + ' ' + boxname +
' posts for ' + nickname + '@' + domain)
print(nickname + '@' + domain + ' has ' + str(noOfPosts) +
' in ' + boxname)
2019-07-03 10:31:02 +00:00
2020-04-04 10:05:27 +00:00
def getPublicPostsOfPerson(baseDir: str, nickname: str, domain: str,
2020-06-09 11:03:59 +00:00
raw: bool, simple: bool, proxyType: str,
2020-04-04 10:05:27 +00:00
port: int, httpPrefix: str,
debug: bool, projectVersion: str) -> None:
2019-07-03 10:31:02 +00:00
""" This is really just for test purposes
"""
2020-06-24 09:04:58 +00:00
print('Starting new session for getting public posts')
2020-06-09 11:03:59 +00:00
session = createSession(proxyType)
if not session:
return
2020-04-04 10:05:27 +00:00
personCache = {}
cachedWebfingers = {}
federationList = []
2019-07-03 10:31:02 +00:00
2020-04-04 10:05:27 +00:00
domainFull = domain
if port:
2020-04-04 10:05:27 +00:00
if port != 80 and port != 443:
if ':' not in domain:
2020-04-04 10:05:27 +00:00
domainFull = domain + ':' + str(port)
handle = httpPrefix + "://" + domainFull + "/@" + nickname
wfRequest = \
webfingerHandle(session, handle, httpPrefix, cachedWebfingers,
domain, projectVersion)
2019-07-03 10:31:02 +00:00
if not wfRequest:
sys.exit()
2020-06-23 10:41:12 +00:00
if not isinstance(wfRequest, dict):
print('Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
sys.exit()
2019-07-03 10:31:02 +00:00
2020-04-04 10:05:27 +00:00
(personUrl, pubKeyId, pubKey,
personId, shaedInbox,
capabilityAcquisition,
avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix,
nickname, domain, 'outbox')
maxMentions = 10
maxEmoji = 10
maxAttachments = 5
getPosts(session, personUrl, 30, maxMentions, maxEmoji,
maxAttachments, federationList,
personCache, raw, simple, debug,
projectVersion, httpPrefix, domain)
def sendCapabilitiesUpdate(session, baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
followerUrl, updateCaps: [],
sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {},
federationList: [], debug: bool,
2019-08-14 20:12:27 +00:00
projectVersion: str) -> int:
2019-07-09 14:20:23 +00:00
"""When the capabilities for a follower are changed this
sends out an update. followerUrl is the actor of the follower.
"""
2020-04-04 10:05:27 +00:00
updateJson = \
capabilitiesUpdate(baseDir, httpPrefix,
nickname, domain, port,
followerUrl, updateCaps)
2019-07-09 14:20:23 +00:00
if not updateJson:
return 1
if debug:
pprint(updateJson)
2020-04-04 10:05:27 +00:00
print('DEBUG: sending capabilities update from ' +
nickname + '@' + domain + ' port ' + str(port) +
' to ' + followerUrl)
2019-07-09 14:20:23 +00:00
2020-04-04 10:05:27 +00:00
clientToServer = False
followerNickname = getNicknameFromActor(followerUrl)
2019-09-02 09:43:43 +00:00
if not followerNickname:
2020-04-04 10:05:27 +00:00
print('WARN: unable to find nickname in ' + followerUrl)
2019-09-02 09:43:43 +00:00
return 1
2020-04-04 10:05:27 +00:00
followerDomain, followerPort = getDomainFromActor(followerUrl)
return sendSignedJson(updateJson, session, baseDir,
nickname, domain, port,
followerNickname, followerDomain, followerPort, '',
httpPrefix, True, clientToServer,
federationList,
sendThreads, postLog, cachedWebfingers,
personCache, debug, projectVersion)
def populateRepliesJson(baseDir: str, nickname: str, domain: str,
postRepliesFilename: str, authorized: bool,
2019-09-28 11:29:42 +00:00
repliesJson: {}) -> None:
2020-04-04 10:05:27 +00:00
pubStr = 'https://www.w3.org/ns/activitystreams#Public'
2019-08-02 18:37:23 +00:00
# populate the items list with replies
2020-04-04 10:05:27 +00:00
repliesBoxes = ('outbox', 'inbox')
with open(postRepliesFilename, 'r') as repliesFile:
2019-08-02 18:37:23 +00:00
for messageId in repliesFile:
2020-04-04 10:05:27 +00:00
replyFound = False
2019-08-02 18:37:23 +00:00
# examine inbox and outbox
for boxname in repliesBoxes:
2020-05-22 11:32:38 +00:00
messageId2 = messageId.replace('\n', '').replace('\r', '')
2020-04-04 10:05:27 +00:00
searchFilename = \
baseDir + \
'/accounts/' + nickname + '@' + \
domain+'/' + \
boxname+'/' + \
2020-05-22 11:32:38 +00:00
messageId2.replace('/', '#') + '.json'
2019-08-02 18:37:23 +00:00
if os.path.isfile(searchFilename):
if authorized or \
2020-04-04 10:05:27 +00:00
pubStr in open(searchFilename).read():
postJsonObject = loadJson(searchFilename)
2019-10-22 11:55:06 +00:00
if postJsonObject:
2020-03-22 21:16:02 +00:00
if postJsonObject['object'].get('cc'):
2020-04-04 10:05:27 +00:00
pjo = postJsonObject
if (authorized or
(pubStr in pjo['object']['to'] or
pubStr in pjo['object']['cc'])):
repliesJson['orderedItems'].append(pjo)
replyFound = True
2019-08-02 18:37:23 +00:00
else:
if authorized or \
2020-04-04 10:05:27 +00:00
pubStr in postJsonObject['object']['to']:
pjo = postJsonObject
repliesJson['orderedItems'].append(pjo)
replyFound = True
2019-08-02 18:37:23 +00:00
break
# if not in either inbox or outbox then examine the shared inbox
if not replyFound:
2020-05-22 11:32:38 +00:00
messageId2 = messageId.replace('\n', '').replace('\r', '')
2020-04-04 10:05:27 +00:00
searchFilename = \
baseDir + \
'/accounts/inbox@' + \
domain+'/inbox/' + \
2020-05-22 11:32:38 +00:00
messageId2.replace('/', '#') + '.json'
2019-08-02 18:37:23 +00:00
if os.path.isfile(searchFilename):
if authorized or \
2020-04-04 10:05:27 +00:00
pubStr in open(searchFilename).read():
# get the json of the reply and append it to
# the collection
postJsonObject = loadJson(searchFilename)
2019-10-22 11:55:06 +00:00
if postJsonObject:
2020-03-22 21:16:02 +00:00
if postJsonObject['object'].get('cc'):
2020-04-04 10:05:27 +00:00
pjo = postJsonObject
if (authorized or
(pubStr in pjo['object']['to'] or
pubStr in pjo['object']['cc'])):
pjo = postJsonObject
repliesJson['orderedItems'].append(pjo)
2019-08-02 18:37:23 +00:00
else:
if authorized or \
2020-04-04 10:05:27 +00:00
pubStr in postJsonObject['object']['to']:
pjo = postJsonObject
repliesJson['orderedItems'].append(pjo)
2019-09-28 16:10:45 +00:00
2019-09-28 16:58:21 +00:00
def rejectAnnounce(announceFilename: str):
"""Marks an announce as rejected
"""
2020-04-04 10:05:27 +00:00
if not os.path.isfile(announceFilename + '.reject'):
rejectAnnounceFile = open(announceFilename + '.reject', "w+")
2019-09-28 16:58:21 +00:00
rejectAnnounceFile.write('\n')
rejectAnnounceFile.close()
2020-04-04 10:05:27 +00:00
def downloadAnnounce(session, baseDir: str, httpPrefix: str,
nickname: str, domain: str,
2020-06-12 11:50:49 +00:00
postJsonObject: {}, projectVersion: str,
translate: {}) -> {}:
2019-09-28 16:10:45 +00:00
"""Download the post referenced by an announce
"""
if not postJsonObject.get('object'):
return None
if not isinstance(postJsonObject['object'], str):
return None
# get the announced post
2020-04-04 10:05:27 +00:00
announceCacheDir = baseDir + '/cache/announce/' + nickname
2019-09-28 16:10:45 +00:00
if not os.path.isdir(announceCacheDir):
os.mkdir(announceCacheDir)
2020-04-04 10:05:27 +00:00
announceFilename = \
announceCacheDir + '/' + \
postJsonObject['object'].replace('/', '#') + '.json'
2019-09-28 16:10:45 +00:00
2020-04-04 10:05:27 +00:00
if os.path.isfile(announceFilename + '.reject'):
2019-09-28 16:10:45 +00:00
return None
if os.path.isfile(announceFilename):
2020-04-04 10:05:27 +00:00
print('Reading cached Announce content for ' +
postJsonObject['object'])
postJsonObject = loadJson(announceFilename)
2019-10-22 11:55:06 +00:00
if postJsonObject:
return postJsonObject
2019-09-28 16:10:45 +00:00
else:
2020-04-04 10:05:27 +00:00
profileStr = 'https://www.w3.org/ns/activitystreams'
asHeader = {
'Accept': 'application/activity+json; profile="' + profileStr + '"'
2020-03-31 11:07:58 +00:00
}
2019-10-18 09:28:00 +00:00
if '/channel/' in postJsonObject['actor']:
2020-04-04 10:05:27 +00:00
asHeader = {
'Accept': 'application/ld+json; profile="' + profileStr + '"'
2020-03-31 11:07:58 +00:00
}
2020-04-04 10:05:27 +00:00
actorNickname = getNicknameFromActor(postJsonObject['actor'])
actorDomain, actorPort = getDomainFromActor(postJsonObject['actor'])
if not actorDomain:
2020-04-04 10:05:27 +00:00
print('Announce actor does not contain a ' +
'valid domain or port number: ' +
str(postJsonObject['actor']))
return None
2020-04-04 10:05:27 +00:00
if isBlocked(baseDir, nickname, domain, actorNickname, actorDomain):
print('Announce download blocked actor: ' +
actorNickname + '@' + actorDomain)
2020-01-18 10:39:51 +00:00
return None
2020-04-04 10:05:27 +00:00
objectNickname = getNicknameFromActor(postJsonObject['object'])
objectDomain, objectPort = getDomainFromActor(postJsonObject['object'])
if not objectDomain:
2020-04-04 10:05:27 +00:00
print('Announce object does not contain a ' +
'valid domain or port number: ' +
str(postJsonObject['object']))
return None
2020-04-04 10:05:27 +00:00
if isBlocked(baseDir, nickname, domain, objectNickname, objectDomain):
2020-02-19 18:55:29 +00:00
if objectNickname and objectDomain:
2020-04-04 10:05:27 +00:00
print('Announce download blocked object: ' +
objectNickname + '@' + objectDomain)
2020-02-19 18:55:29 +00:00
else:
2020-04-04 10:05:27 +00:00
print('Announce download blocked object: ' +
2020-03-31 11:07:58 +00:00
str(postJsonObject['object']))
2020-02-05 11:46:05 +00:00
return None
2020-04-04 10:05:27 +00:00
print('Downloading Announce content for ' + postJsonObject['object'])
announcedJson = \
getJson(session, postJsonObject['object'], asHeader,
None, projectVersion, httpPrefix, domain)
2020-01-19 20:19:56 +00:00
2019-09-28 16:10:45 +00:00
if not announcedJson:
return None
2019-12-04 09:44:41 +00:00
if not isinstance(announcedJson, dict):
2020-04-04 10:05:27 +00:00
print('WARN: announce json is not a dict - ' +
2020-03-31 11:07:58 +00:00
postJsonObject['object'])
2019-12-04 09:47:35 +00:00
rejectAnnounce(announceFilename)
2020-03-22 21:16:02 +00:00
return None
2019-09-28 16:10:45 +00:00
if not announcedJson.get('id'):
rejectAnnounce(announceFilename)
return None
if '/statuses/' not in announcedJson['id']:
rejectAnnounce(announceFilename)
return None
2019-10-17 22:26:47 +00:00
if '/users/' not in announcedJson['id'] and \
'/channel/' not in announcedJson['id'] and \
'/profile/' not in announcedJson['id']:
2019-09-28 16:10:45 +00:00
rejectAnnounce(announceFilename)
return None
if not announcedJson.get('type'):
rejectAnnounce(announceFilename)
2020-04-04 10:05:27 +00:00
# pprint(announcedJson)
2019-09-28 16:10:45 +00:00
return None
2020-04-04 10:05:27 +00:00
if announcedJson['type'] != 'Note' and \
announcedJson['type'] != 'Article':
2019-09-28 16:10:45 +00:00
rejectAnnounce(announceFilename)
2020-04-04 10:05:27 +00:00
# pprint(announcedJson)
2019-09-28 16:10:45 +00:00
return None
2020-02-05 14:57:10 +00:00
if not announcedJson.get('content'):
rejectAnnounce(announceFilename)
2020-03-22 21:16:02 +00:00
return None
2020-04-04 10:05:27 +00:00
if isFiltered(baseDir, nickname, domain, announcedJson['content']):
2020-02-05 14:57:10 +00:00
rejectAnnounce(announceFilename)
return None
2020-05-17 09:44:42 +00:00
# remove any long words
announcedJson['content'] = \
removeLongWords(announcedJson['content'], 40, [])
2020-01-19 20:19:56 +00:00
# remove text formatting, such as bold/italics
announcedJson['content'] = \
removeTextFormatting(announcedJson['content'])
2019-09-28 16:10:45 +00:00
# wrap in create to be consistent with other posts
2020-04-04 10:05:27 +00:00
announcedJson = \
outboxMessageCreateWrap(httpPrefix,
actorNickname, actorDomain, actorPort,
2019-09-28 16:10:45 +00:00
announcedJson)
2020-04-04 10:05:27 +00:00
if announcedJson['type'] != 'Create':
2019-09-28 16:10:45 +00:00
rejectAnnounce(announceFilename)
2020-04-04 10:05:27 +00:00
# pprint(announcedJson)
2019-09-28 16:10:45 +00:00
return None
2020-06-16 20:29:17 +00:00
# labelAccusatoryPost(postJsonObject, translate)
2019-09-28 16:10:45 +00:00
# set the id to the original status
2020-04-04 10:05:27 +00:00
announcedJson['id'] = postJsonObject['object']
announcedJson['object']['id'] = postJsonObject['object']
2019-09-28 16:10:45 +00:00
# check that the repeat isn't for a blocked account
2020-04-04 10:05:27 +00:00
attributedNickname = \
2019-12-12 09:58:06 +00:00
getNicknameFromActor(announcedJson['object']['id'])
2020-04-04 10:05:27 +00:00
attributedDomain, attributedPort = \
2019-12-12 09:58:06 +00:00
getDomainFromActor(announcedJson['object']['id'])
2019-09-28 16:10:45 +00:00
if attributedNickname and attributedDomain:
if attributedPort:
2020-04-04 10:05:27 +00:00
if attributedPort != 80 and attributedPort != 443:
attributedDomain = \
attributedDomain + ':' + str(attributedPort)
if isBlocked(baseDir, nickname, domain,
attributedNickname, attributedDomain):
2019-09-28 16:10:45 +00:00
rejectAnnounce(announceFilename)
return None
2020-04-04 10:05:27 +00:00
postJsonObject = announcedJson
2020-01-15 22:31:04 +00:00
replaceYouTube(postJsonObject)
2020-04-04 10:05:27 +00:00
if saveJson(postJsonObject, announceFilename):
2019-10-22 11:55:06 +00:00
return postJsonObject
2019-09-28 16:10:45 +00:00
return None
2019-12-01 13:45:30 +00:00
2020-04-04 10:05:27 +00:00
def mutePost(baseDir: str, nickname: str, domain: str, postId: str,
2019-12-01 13:45:30 +00:00
recentPostsCache: {}) -> None:
""" Mutes the given post
"""
2020-04-04 10:05:27 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
2019-12-01 13:45:30 +00:00
if not postFilename:
return
2020-04-04 10:05:27 +00:00
postJsonObject = loadJson(postFilename)
2019-12-01 13:45:30 +00:00
if not postJsonObject:
return
2020-04-04 10:05:27 +00:00
print('MUTE: ' + postFilename)
muteFile = open(postFilename + '.muted', "w")
2019-12-01 14:03:18 +00:00
if muteFile:
muteFile.write('\n')
muteFile.close()
2019-12-01 13:45:30 +00:00
# remove cached posts so that the muted version gets created
2020-04-04 10:05:27 +00:00
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
2019-12-01 13:45:30 +00:00
if cachedPostFilename:
if os.path.isfile(cachedPostFilename):
os.remove(cachedPostFilename)
# if the post is in the recent posts cache then mark it as muted
2019-12-01 14:43:09 +00:00
if recentPostsCache.get('index'):
2020-04-04 10:05:27 +00:00
postId = \
postJsonObject['id'].replace('/activity', '').replace('/', '#')
2019-12-01 14:43:09 +00:00
if postId in recentPostsCache['index']:
2020-04-04 10:05:27 +00:00
print('MUTE: ' + postId + ' is in recent posts cache')
2019-12-01 14:43:09 +00:00
if recentPostsCache['json'].get(postId):
2020-04-04 10:05:27 +00:00
postJsonObject['muted'] = True
recentPostsCache['json'][postId] = json.dumps(postJsonObject)
print('MUTE: ' + postId +
' marked as muted in recent posts cache')
2019-12-01 13:45:30 +00:00
2020-04-04 10:05:27 +00:00
def unmutePost(baseDir: str, nickname: str, domain: str, postId: str,
2019-12-01 13:45:30 +00:00
recentPostsCache: {}) -> None:
""" Unmutes the given post
"""
2020-04-04 10:05:27 +00:00
postFilename = locatePost(baseDir, nickname, domain, postId)
2019-12-01 13:45:30 +00:00
if not postFilename:
return
2020-04-04 10:05:27 +00:00
postJsonObject = loadJson(postFilename)
2019-12-01 13:45:30 +00:00
if not postJsonObject:
return
2020-04-04 10:05:27 +00:00
print('UNMUTE: ' + postFilename)
muteFilename = postFilename + '.muted'
2019-12-01 13:45:30 +00:00
if os.path.isfile(muteFilename):
os.remove(muteFilename)
# remove cached posts so that it gets recreated
2020-04-04 10:05:27 +00:00
cachedPostFilename = \
getCachedPostFilename(baseDir, nickname, domain, postJsonObject)
2019-12-01 13:45:30 +00:00
if cachedPostFilename:
if os.path.isfile(cachedPostFilename):
2019-12-01 15:19:11 +00:00
os.remove(cachedPostFilename)
2020-04-04 10:05:27 +00:00
removePostFromCache(postJsonObject, recentPostsCache)
2020-04-01 20:13:42 +00:00
def sendBlockViaServer(baseDir: str, session,
fromNickname: str, password: str,
fromDomain: str, fromPort: int,
httpPrefix: str, blockedUrl: str,
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str) -> {}:
"""Creates a block via c2s
"""
if not session:
print('WARN: No session for sendBlockViaServer')
return 6
fromDomainFull = fromDomain
if fromPort:
if fromPort != 80 and fromPort != 443:
if ':' not in fromDomain:
fromDomainFull = fromDomain + ':' + str(fromPort)
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
ccUrl = httpPrefix + '://' + fromDomainFull + '/users/' + \
fromNickname + '/followers'
blockActor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname
newBlockJson = {
"@context": "https://www.w3.org/ns/activitystreams",
'type': 'Block',
'actor': blockActor,
'object': blockedUrl,
'to': [toUrl],
'cc': [ccUrl]
}
handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
# lookup the inbox for the To handle
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
fromDomain, projectVersion)
if not wfRequest:
if debug:
print('DEBUG: announce webfinger failed for ' + handle)
return 1
2020-06-23 10:41:12 +00:00
if not isinstance(wfRequest, dict):
print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
return 1
2020-04-01 20:13:42 +00:00
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest,
personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
if not inboxUrl:
if debug:
print('DEBUG: No ' + postToBox + ' was found for ' + handle)
return 3
if not fromPersonId:
if debug:
print('DEBUG: No actor was found for ' + handle)
return 4
authHeader = createBasicAuthHeader(fromNickname, password)
headers = {
'host': fromDomain,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newBlockJson, [], inboxUrl,
headers, "inbox:write")
if not postResult:
print('WARN: Unable to post block')
if debug:
print('DEBUG: c2s POST block success')
return newBlockJson
def sendUndoBlockViaServer(baseDir: str, session,
fromNickname: str, password: str,
fromDomain: str, fromPort: int,
httpPrefix: str, blockedUrl: str,
cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str) -> {}:
"""Creates a block via c2s
"""
if not session:
print('WARN: No session for sendBlockViaServer')
return 6
fromDomainFull = fromDomain
if fromPort:
if fromPort != 80 and fromPort != 443:
if ':' not in fromDomain:
fromDomainFull = fromDomain + ':' + str(fromPort)
toUrl = 'https://www.w3.org/ns/activitystreams#Public'
ccUrl = httpPrefix + '://' + fromDomainFull + '/users/' + \
fromNickname + '/followers'
blockActor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname
newBlockJson = {
"@context": "https://www.w3.org/ns/activitystreams",
'type': 'Undo',
'actor': blockActor,
'object': {
'type': 'Block',
'actor': blockActor,
'object': blockedUrl,
'to': [toUrl],
'cc': [ccUrl]
}
}
handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
# lookup the inbox for the To handle
wfRequest = webfingerHandle(session, handle, httpPrefix,
cachedWebfingers,
fromDomain, projectVersion)
if not wfRequest:
if debug:
print('DEBUG: announce webfinger failed for ' + handle)
return 1
2020-06-23 10:41:12 +00:00
if not isinstance(wfRequest, dict):
print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
str(wfRequest))
return 1
2020-04-01 20:13:42 +00:00
postToBox = 'outbox'
# get the actor inbox for the To handle
(inboxUrl, pubKeyId, pubKey,
fromPersonId, sharedInbox,
capabilityAcquisition, avatarUrl,
displayName) = getPersonBox(baseDir, session, wfRequest, personCache,
projectVersion, httpPrefix, fromNickname,
fromDomain, postToBox)
if not inboxUrl:
if debug:
print('DEBUG: No ' + postToBox + ' was found for ' + handle)
return 3
if not fromPersonId:
if debug:
print('DEBUG: No actor was found for ' + handle)
return 4
authHeader = createBasicAuthHeader(fromNickname, password)
headers = {
'host': fromDomain,
'Content-type': 'application/json',
'Authorization': authHeader
}
postResult = postJson(session, newBlockJson, [], inboxUrl,
headers, "inbox:write")
if not postResult:
print('WARN: Unable to post block')
if debug:
print('DEBUG: c2s POST block success')
return newBlockJson