epicyon/posts.py

2638 lines
104 KiB
Python
Raw Normal View History

2019-06-28 18:55:29 +00:00
__filename__ = "posts.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2019-08-29 13:35:29 +00:00
__version__ = "1.0.0"
2019-06-28 18:55:29 +00:00
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
import requests
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
2019-06-30 13:20:23 +00:00
import threading
2019-06-30 15:03:26 +00:00
import sys
import trace
2019-07-01 11:48:54 +00:00
import time
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-06-28 18:55:29 +00:00
from random import randint
2019-07-03 10:33:55 +00:00
from session import createSession
2019-06-28 18:55:29 +00:00
from session import getJson
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
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
2019-08-09 09:09:21 +00:00
from content import addHtmlTags
2019-09-29 17:42:51 +00:00
from content import replaceEmojiFromTags
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
2019-06-28 18:55:29 +00:00
try:
from BeautifulSoup import BeautifulSoup
except ImportError:
from bs4 import BeautifulSoup
2019-08-12 13:22:17 +00:00
def isModerator(baseDir: str,nickname: str) -> bool:
"""Returns true if the given nickname is a moderator
"""
moderatorsFile=baseDir+'/accounts/moderators.txt'
if not os.path.isfile(moderatorsFile):
if getConfigParam(baseDir,'admin')==nickname:
return True
return False
with open(moderatorsFile, "r") as f:
lines = f.readlines()
if len(lines)==0:
if getConfigParam(baseDir,'admin')==nickname:
return True
for moderator in lines:
moderator=moderator.strip('\n')
if moderator==nickname:
return True
return False
2019-07-06 17:00:22 +00:00
def noOfFollowersOnDomain(baseDir: str,handle: str, \
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
"""
filename=baseDir+'/accounts/'+handle+'/'+followFile
if not os.path.isfile(filename):
return 0
ctr=0
with open(filename, "r") as followersFilename:
for followerHandle in followersFilename:
if '@' in followerHandle:
2019-07-06 17:00:22 +00:00
followerDomain= \
followerHandle.split('@')[1].replace('\n','')
2019-07-05 14:39:24 +00:00
if domain==followerDomain:
ctr+=1
return ctr
2019-07-06 17:00:22 +00:00
def getPersonKey(nickname: str,domain: str,baseDir: str,keyType='public', \
debug=False):
2019-06-30 15:03:26 +00:00
"""Returns the public or private key of a person
"""
2019-07-03 09:40:27 +00:00
handle=nickname+'@'+domain
2019-06-30 15:03:26 +00:00
keyFilename=baseDir+'/keys/'+keyType+'/'+handle.lower()+'.key'
if not os.path.isfile(keyFilename):
2019-07-06 13:49:25 +00:00
if debug:
print('DEBUG: private key file not found: '+keyFilename)
2019-06-30 15:03:26 +00:00
return ''
keyPem=''
with open(keyFilename, "r") as pemFile:
keyPem=pemFile.read()
if len(keyPem)<20:
2019-07-06 13:49:25 +00:00
if debug:
print('DEBUG: private key was too short: '+keyPem)
2019-06-30 15:03:26 +00:00
return ''
return keyPem
2019-06-28 18:55:29 +00:00
def cleanHtml(rawHtml: str) -> str:
text = BeautifulSoup(rawHtml, 'html.parser').get_text()
return html.unescape(text)
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':
2019-10-21 16:15:12 +00:00
if not ('/users/' in link['href'] or \
'/profile/' in link['href'] or \
'/channel/' in link['href']):
print('Webfinger activity+json contains single user instance actor')
return link['href']
2019-06-28 18:55:29 +00:00
return None
2019-08-14 20:12: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
nextUrl = None
2019-06-29 10:08:59 +00:00
if 'first' in feedJson:
2019-06-29 10:59:16 +00:00
nextUrl = feedJson['first']
2019-06-29 10:08:59 +00:00
elif 'next' in feedJson:
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):
userFeed=parseUserFeed(session,nextUrl,asHeader, \
projectVersion,httpPrefix,domain)
for item in userFeed:
yield item
elif isinstance(nextUrl, dict):
userFeed=nextUrl
if userFeed.get('orderedItems'):
for item in userFeed['orderedItems']:
yield item
2019-06-30 11:34:19 +00:00
2019-08-20 09:16:03 +00:00
def getPersonBox(baseDir: str,session,wfRequest: {},personCache: {}, \
2019-10-17 15:55:05 +00:00
projectVersion: str,httpPrefix: str, \
nickname: str,domain: str, \
2019-07-23 12:33:09 +00:00
boxName='inbox') -> (str,str,str,str,str,str,str,str):
2019-09-01 11:52:46 +00:00
asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-10-17 15:55:05 +00:00
if not wfRequest.get('errors'):
personUrl = getUserUrl(wfRequest)
else:
2019-10-21 16:03:44 +00:00
if nickname=='dev':
# try single user instance
print('getPersonBox: Trying single user instance with ld+json')
personUrl = httpPrefix+'://'+domain
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
else:
personUrl = httpPrefix+'://'+domain+'/users/'+nickname
2019-06-30 10:14:02 +00:00
if not personUrl:
2019-10-21 16:03:44 +00:00
return None,None,None,None,None,None,None,None
2019-08-20 09:37:09 +00:00
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:
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-08-14 20:12:27 +00:00
personJson = getJson(session,personUrl,asHeader,None, \
projectVersion,httpPrefix,domain)
2019-07-04 17:31:41 +00:00
if not personJson:
2019-10-21 16:20:33 +00:00
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
personJson = getJson(session,personUrl,asHeader,None, \
projectVersion,httpPrefix,domain)
if not personJson:
2019-10-21 16:21:16 +00:00
print('Unable to get actor')
2019-10-21 16:20:33 +00:00
return None,None,None,None,None,None,None,None
2019-07-05 13:38:29 +00:00
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):
boxJson=personJson['endpoints'][boxName]
else:
boxJson=personJson[boxName]
if not boxJson:
2019-07-23 12:33:09 +00:00
return None,None,None,None,None,None,None,None
2019-07-05 13:38:29 +00:00
2019-06-30 10:14:02 +00:00
personId=None
if personJson.get('id'):
personId=personJson['id']
2019-07-01 10:25:03 +00:00
pubKeyId=None
2019-06-30 10:14:02 +00:00
pubKey=None
if personJson.get('publicKey'):
2019-07-01 10:25:03 +00:00
if personJson['publicKey'].get('id'):
pubKeyId=personJson['publicKey']['id']
2019-06-30 10:14:02 +00:00
if personJson['publicKey'].get('publicKeyPem'):
pubKey=personJson['publicKey']['publicKeyPem']
2019-07-05 13:50:27 +00:00
sharedInbox=None
if personJson.get('sharedInbox'):
sharedInbox=personJson['sharedInbox']
else:
if personJson.get('endpoints'):
if personJson['endpoints'].get('sharedInbox'):
sharedInbox=personJson['endpoints']['sharedInbox']
2019-07-05 20:32:21 +00:00
capabilityAcquisition=None
2019-07-06 17:00:22 +00:00
if personJson.get('capabilityAcquisitionEndpoint'):
capabilityAcquisition=personJson['capabilityAcquisitionEndpoint']
2019-07-22 14:21:49 +00:00
avatarUrl=None
2019-07-22 14:09:21 +00:00
if personJson.get('icon'):
if personJson['icon'].get('url'):
2019-07-22 14:21:49 +00:00
avatarUrl=personJson['icon']['url']
displayName=None
if personJson.get('name'):
displayName=personJson['name']
2019-06-30 10:21:07 +00:00
2019-08-20 09:16:03 +00:00
storePersonInCache(baseDir,personUrl,personJson,personCache)
2019-06-30 10:21:07 +00:00
return boxJson,pubKeyId,pubKey,personId,sharedInbox,capabilityAcquisition,avatarUrl,displayName
2019-06-30 10:14:02 +00:00
2019-07-19 16:56:55 +00:00
def getPosts(session,outboxUrl: str,maxPosts: int, \
maxMentions: int, \
2019-07-06 10:33:57 +00:00
maxEmoji: int,maxAttachments: int, \
2019-07-19 16:56:55 +00:00
federationList: [], \
personCache: {},raw: bool, \
2019-08-14 20:12:27 +00:00
simple: bool,debug: bool, \
projectVersion: str,httpPrefix: str,domain: str) -> {}:
2019-07-28 11:08:14 +00:00
"""Gets public posts from an outbox
"""
2019-09-01 13:13:52 +00:00
personPosts={}
2019-07-02 09:25:29 +00:00
if not outboxUrl:
return personPosts
2019-09-01 12:09:29 +00:00
asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-10-17 22:26:47 +00:00
if '/outbox/' in outboxUrl:
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-07-03 11:24:38 +00:00
if raw:
result = []
i = 0
2019-09-01 13:13:52 +00:00
userFeed=parseUserFeed(session,outboxUrl,asHeader, \
projectVersion,httpPrefix,domain)
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
2019-06-28 18:55:29 +00:00
i = 0
2019-09-01 13:13:52 +00:00
userFeed=parseUserFeed(session,outboxUrl,asHeader, \
projectVersion,httpPrefix,domain)
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
#pprint(item)
2019-06-28 18:55:29 +00:00
published = item['object']['published']
2019-07-19 16:56:55 +00:00
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'):
isPublic=False
for recipient in item['object']['to']:
if recipient.endswith('#Public'):
isPublic=True
break
if not isPublic:
continue
2019-08-09 08:46:38 +00:00
content = item['object']['content'].replace('&apos;',"'")
2019-06-28 18:55:29 +00:00
mentions=[]
emoji={}
if item['object'].get('tag'):
for tagItem in item['object']['tag']:
tagType=tagItem['type'].lower()
if tagType=='emoji':
if tagItem.get('name') and tagItem.get('icon'):
if tagItem['icon'].get('url'):
# No emoji from non-permitted domains
2019-07-06 17:00:22 +00:00
if urlPermitted(tagItem['icon']['url'], \
2019-07-09 14:20:23 +00:00
federationList, \
2019-07-06 17:00:22 +00:00
"objects:read"):
2019-06-28 18:55:29 +00:00
emojiName=tagItem['name']
emojiIcon=tagItem['icon']['url']
emoji[emojiName]=emojiIcon
2019-07-19 16:56:55 +00:00
else:
if debug:
print('url not permitted '+tagItem['icon']['url'])
2019-06-28 18:55:29 +00:00
if tagType=='mention':
if tagItem.get('name'):
if tagItem['name'] not in mentions:
mentions.append(tagItem['name'])
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
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
summary = ''
if item['object'].get('summary'):
if item['object']['summary']:
summary = item['object']['summary']
inReplyTo = ''
if item['object'].get('inReplyTo'):
if item['object']['inReplyTo']:
# No replies to non-permitted domains
2019-07-06 17:00:22 +00:00
if not urlPermitted(item['object']['inReplyTo'], \
2019-07-09 14:20:23 +00:00
federationList, \
2019-07-06 17:00:22 +00:00
"objects:read"):
2019-07-19 16:56:55 +00:00
if debug:
print('url not permitted '+item['object']['inReplyTo'])
2019-06-28 18:55:29 +00:00
continue
inReplyTo = item['object']['inReplyTo']
conversation = ''
if item['object'].get('conversation'):
if item['object']['conversation']:
# no conversations originated in non-permitted domains
2019-07-06 17:00:22 +00:00
if urlPermitted(item['object']['conversation'], \
2019-07-09 14:20:23 +00:00
federationList,"objects:read"):
2019-06-28 18:55:29 +00:00
conversation = item['object']['conversation']
attachment = []
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
2019-07-06 17:00:22 +00:00
if urlPermitted(attach['url'], \
2019-07-09 14:20:23 +00:00
federationList, \
2019-07-06 17:00:22 +00:00
"objects:read"):
2019-06-28 18:55:29 +00:00
attachment.append([attach['name'],attach['url']])
2019-07-19 16:56:55 +00:00
else:
if debug:
print('url not permitted '+attach['url'])
2019-06-28 18:55:29 +00:00
sensitive = False
if item['object'].get('sensitive'):
sensitive = item['object']['sensitive']
2019-07-03 11:24:38 +00:00
if simple:
print(cleanHtml(content)+'\n')
else:
2019-07-19 16:56:55 +00:00
pprint(item)
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
2019-07-04 16:24:23 +00:00
def deleteAllPosts(baseDir: str,nickname: str, domain: str,boxname: str) -> None:
"""Deletes all posts for a person from inbox or outbox
2019-06-29 11:47:33 +00:00
"""
2019-07-04 16:24:23 +00:00
if boxname!='inbox' and boxname!='outbox':
return
boxDir = createPersonDir(nickname,domain,baseDir,boxname)
2019-09-27 12:09:04 +00:00
for deleteFilename in os.scandir(boxDir):
deleteFilename=deleteFilename.name
2019-07-04 16:24:23 +00:00
filePath = os.path.join(boxDir, deleteFilename)
2019-06-29 11:47:33 +00:00
try:
if os.path.isfile(filePath):
os.unlink(filePath)
elif os.path.isdir(filePath): shutil.rmtree(filePath)
except Exception as e:
print(e)
2019-07-06 17:00:22 +00:00
def savePostToBox(baseDir: str,httpPrefix: str,postId: str, \
2019-07-14 16:57:06 +00:00
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
"""
2019-07-04 16:24:23 +00:00
if boxname!='inbox' and boxname!='outbox':
return None
2019-07-18 11:35:48 +00:00
originalDomain=domain
if ':' in domain:
domain=domain.split(':')[0]
2019-07-04 16:24:23 +00:00
2019-07-03 22:59:56 +00:00
if not postId:
statusNumber,published = getStatusNumber()
2019-08-08 08:44:37 +00:00
postId=httpPrefix+'://'+originalDomain+'/users/'+nickname+'/statuses/'+statusNumber
2019-07-14 16:57:06 +00:00
postJsonObject['id']=postId+'/activity'
if postJsonObject.get('object'):
2019-07-16 19:07:45 +00:00
if isinstance(postJsonObject['object'], dict):
postJsonObject['object']['id']=postId
postJsonObject['object']['atomUri']=postId
2019-07-03 22:59:56 +00:00
2019-07-04 16:24:23 +00:00
boxDir = createPersonDir(nickname,domain,baseDir,boxname)
filename=boxDir+'/'+postId.replace('/','#')+'.json'
2019-10-22 11:55:06 +00:00
saveJson(postJsonObject,filename)
return filename
2019-08-09 11:12:08 +00:00
def updateHashtagsIndex(baseDir: str,tag: {},newPostId: str) -> None:
"""Writes the post url for hashtags to a file
This allows posts for a hashtag to be quickly looked up
"""
2019-08-09 17:42:11 +00:00
if tag['type']!='Hashtag':
return
# create hashtags directory
2019-08-09 11:12:08 +00:00
tagsDir=baseDir+'/tags'
if not os.path.isdir(tagsDir):
os.mkdir(tagsDir)
tagName=tag['name']
tagsFilename=tagsDir+'/'+tagName[1:]+'.txt'
tagFile=open(tagsFilename, "a+")
if not tagFile:
return
tagFile.write(newPostId+'\n')
tagFile.close()
def createPostBase(baseDir: str,nickname: str,domain: str,port: int, \
toUrl: str,ccUrl: str,httpPrefix: str,content: str, \
followersOnly: bool,saveToFile: bool,clientToServer: bool, \
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str,imageDescription: str, \
2019-08-11 20:38:10 +00:00
useBlurhash: bool,isModerationReport: bool,inReplyTo=None, \
inReplyToAtomUri=None,subject=None, \
2019-10-10 13:13:59 +00:00
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
"""
2019-08-19 09:37:14 +00:00
mentionedRecipients= \
getMentionedPeople(baseDir,httpPrefix,content,domain,False)
2019-08-09 11:12:08 +00:00
tags=[]
hashtagsDict={}
2019-07-15 14:41:15 +00:00
if port:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
2019-07-01 12:47:08 +00:00
2019-08-10 16:55:53 +00:00
# convert content to html
2019-09-29 17:42:51 +00:00
emojisDict={}
2019-11-01 10:19:21 +00:00
# replace 's
content=content.replace("\ufffd\ufffd\ufffds","'s").replace("","'")
# add tags
2019-08-09 16:18:00 +00:00
content= \
addHtmlTags(baseDir,httpPrefix, \
nickname,domain,content, \
mentionedRecipients, \
hashtagsDict,True)
2019-08-19 10:56:49 +00:00
2019-06-29 22:29:18 +00:00
statusNumber,published = getStatusNumber()
2019-06-28 18:55:29 +00:00
postTo='https://www.w3.org/ns/activitystreams#Public'
2019-07-03 19:00:03 +00:00
postCC=httpPrefix+'://'+domain+'/users/'+nickname+'/followers'
2019-06-28 18:55:29 +00:00
if followersOnly:
postTo=postCC
postCC=''
2019-07-03 19:00:03 +00:00
newPostId=httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
2019-08-11 20:38:10 +00:00
2019-06-29 10:23:40 +00:00
sensitive=False
2019-06-30 21:20:02 +00:00
summary=None
2019-06-29 10:23:40 +00:00
if subject:
summary=subject
sensitive=True
2019-07-15 14:41:15 +00:00
2019-08-19 10:52:38 +00:00
toRecipients=[]
2019-11-04 12:09:59 +00:00
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
2019-08-19 10:52:38 +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:
isPublic=False
for recipient in toRecipients:
if recipient.endswith('#Public'):
isPublic=True
break
2019-08-09 11:12:08 +00:00
for tagName,tag in hashtagsDict.items():
tags.append(tag)
if isPublic:
updateHashtagsIndex(baseDir,tag,newPostId)
2019-09-29 19:39:35 +00:00
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
2019-10-02 15:00:22 +00:00
replyPostFilename=locatePost(baseDir,nickname,domain,inReplyTo)
if replyPostFilename:
2019-10-22 11:55:06 +00:00
replyToJson=loadJson(replyPostFilename)
if replyToJson:
if replyToJson.get('object'):
if replyToJson['object'].get('sensitive'):
if replyToJson['object']['sensitive']:
sensitive=True
if replyToJson['object'].get('summary'):
summary=replyToJson['object']['summary']
2019-10-10 13:12:13 +00:00
if eventDate:
eventName=summary
if not eventName:
eventName=content
eventDateStr=eventDate
if eventTime:
2019-10-10 13:24:29 +00:00
if eventTime.endswith('Z'):
eventDateStr=eventDate+'T'+eventTime
else:
2019-10-10 13:48:05 +00:00
eventDateStr=eventDate+'T'+eventTime+':00'+strftime("%z", gmtime())
2019-10-10 13:12:13 +00:00
else:
2019-10-10 13:48:05 +00:00
eventDateStr=eventDate+'T12:00:00Z'
2019-10-10 13:12:13 +00:00
tags.append({
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Event",
"name": eventName,
"startTime": eventDateStr,
"endTime": eventDateStr
})
if location:
tags.append({
"@context": "https://www.w3.org/ns/activitystreams",
"type": "Place",
"name": location
})
2019-10-19 15:59:49 +00:00
postContext=[
'https://www.w3.org/ns/activitystreams',
{
'Hashtag': 'as:Hashtag',
'sensitive': 'as:sensitive',
'toot': 'http://joinmastodon.org/ns#',
'votersCount': 'toot:votersCount'
}
]
2019-07-03 15:10:18 +00:00
if not clientToServer:
2019-07-06 10:33:57 +00:00
actorUrl=httpPrefix+'://'+domain+'/users/'+nickname
2019-07-07 11:53:32 +00:00
# if capabilities have been granted for this actor
# then get the corresponding id
capabilityId=None
2019-07-08 13:30:04 +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):
2019-10-22 11:55:06 +00:00
oc=loadJson(ocapFilename)
if oc:
if oc.get('id'):
capabilityIdList=[oc['id']]
2019-07-03 15:10:18 +00:00
newPost = {
2019-10-19 15:59:49 +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,
'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': {
'id': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
'type': 'Collection',
'first': {
'type': 'CollectionPage',
'partOf': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
'items': []
}
}
2019-07-03 15:10:18 +00:00
}
}
2019-07-12 19:08:46 +00:00
if attachImageFilename:
newPost['object']= \
2019-08-30 18:32:34 +00:00
attachMedia(baseDir,httpPrefix,domain,port, \
2019-07-12 19:26:54 +00:00
newPost['object'],attachImageFilename, \
2019-08-30 15:50:20 +00:00
mediaType,imageDescription,useBlurhash)
2019-07-03 15:10:18 +00:00
else:
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,
'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': {
'id': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
'type': 'Collection',
'first': {
'type': 'CollectionPage',
'partOf': 'https://'+domain+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
'items': []
}
}
2019-06-28 18:55:29 +00:00
}
2019-07-12 19:08:46 +00:00
if attachImageFilename:
newPost= \
2019-08-30 18:32:34 +00:00
attachMedia(baseDir,httpPrefix,domain,port, \
2019-07-12 19:26:54 +00:00
newPost,attachImageFilename, \
2019-08-30 15:50:20 +00:00
mediaType,imageDescription,useBlurhash)
2019-07-01 12:14:49 +00:00
if ccUrl:
if len(ccUrl)>0:
2019-07-19 18:18:06 +00:00
newPost['cc']=[ccUrl]
2019-07-19 18:12:50 +00:00
if newPost.get('object'):
2019-07-19 18:18:06 +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'):
newPost['object']['moderationStatus']='pending'
else:
newPost['moderationStatus']='pending'
2019-08-12 13:22:17 +00:00
# save to index file
moderationIndexFile=baseDir+'/accounts/moderation.txt'
modFile=open(moderationIndexFile, "a+")
if modFile:
modFile.write(newPostId+'\n')
modFile.close()
2019-08-11 20:38:10 +00:00
2019-06-29 10:08:59 +00:00
if saveToFile:
2019-07-06 17:00:22 +00:00
savePostToBox(baseDir,httpPrefix,newPostId, \
nickname,domain,newPost,'outbox')
2019-06-28 18:55:29 +00:00
return newPost
2019-06-29 10:08:59 +00:00
2019-07-16 10:19:04 +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:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
2019-07-03 21:37:46 +00:00
statusNumber,published = getStatusNumber()
if messageJson.get('published'):
published = messageJson['published']
newPostId=httpPrefix+'://'+domain+'/users/'+nickname+'/statuses/'+statusNumber
cc=[]
if messageJson.get('cc'):
cc=messageJson['cc']
2019-07-05 20:46:47 +00:00
# TODO
2019-07-16 10:19:04 +00:00
capabilityUrl=[]
2019-07-03 21:37:46 +00:00
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
}
newPost['object']['id']=newPost['id']
2019-07-06 17:00:22 +00:00
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
2019-07-08 13:30:04 +00:00
def postIsAddressedToFollowers(baseDir: str,
2019-10-11 16:16:56 +00:00
nickname: str,domain: str,port: int,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:
if port!=80 and port!=443:
if ':' not in domain:
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
2019-07-16 19:07:45 +00:00
toList=[]
ccList=[]
2019-08-20 21:09:56 +00:00
if postJsonObject['type']!='Update' and \
isinstance(postJsonObject['object'], dict):
2019-11-04 12:46:51 +00:00
if postJsonObject['object'].get('to'):
toList=postJsonObject['object']['to']
2019-07-16 19:07:45 +00:00
if postJsonObject['object'].get('cc'):
ccList=postJsonObject['object']['cc']
else:
2019-11-04 12:46:51 +00:00
if postJsonObject.get('to'):
toList=postJsonObject['to']
2019-07-16 19:07:45 +00:00
if postJsonObject.get('cc'):
ccList=postJsonObject['cc']
2019-07-08 13:30:04 +00:00
followersUrl=httpPrefix+'://'+domain+'/users/'+nickname+'/followers'
# does the followers url exist in 'to' or 'cc' lists?
addressedToFollowers=False
2019-07-16 19:07:45 +00:00
if followersUrl in toList:
2019-07-08 13:30:04 +00:00
addressedToFollowers=True
2019-11-04 12:46:51 +00:00
elif followersUrl in ccList:
addressedToFollowers=True
2019-07-08 13:30:04 +00:00
return addressedToFollowers
2019-07-15 17:22:51 +00:00
def postIsAddressedToPublic(baseDir: str,postJsonObject: {}) -> bool:
"""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
publicUrl='https://www.w3.org/ns/activitystreams#Public'
# does the public url exist in 'to' or 'cc' lists?
addressedToPublic=False
if publicUrl in postJsonObject['object']['to']:
addressedToPublic=True
if not addressedToPublic:
if not postJsonObject['object'].get('cc'):
return False
if publicUrl in postJsonObject['object']['cc']:
addressedToPublic=True
return addressedToPublic
2019-07-03 15:10:18 +00:00
def createPublicPost(baseDir: str,
2019-10-10 13:12:13 +00:00
nickname: str,domain: str,port: int,httpPrefix: str, \
content: str,followersOnly: bool,saveToFile: bool,
2019-07-09 14:20:23 +00:00
clientToServer: bool,\
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-10-10 13:12:13 +00:00
inReplyTo=None,inReplyToAtomUri=None,subject=None, \
eventDate=None,eventTime=None,location=None) -> {}:
2019-07-27 22:48:34 +00:00
"""Public post
2019-06-30 10:14:02 +00:00
"""
2019-07-28 18:06:20 +00:00
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-10-10 13:12:13 +00:00
return createPostBase(baseDir,nickname,domain,port, \
2019-07-02 20:54:22 +00:00
'https://www.w3.org/ns/activitystreams#Public', \
2019-07-28 18:06:20 +00:00
httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers', \
2019-10-10 13:12:13 +00:00
httpPrefix,content,followersOnly,saveToFile, \
2019-07-09 14:20:23 +00:00
clientToServer, \
2019-08-30 15:50:20 +00:00
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
2019-10-10 13:12:13 +00:00
False,inReplyTo,inReplyToAtomUri,subject, \
eventDate,eventTime,location)
2019-07-02 20:54:22 +00:00
2019-11-25 22:34:26 +00:00
def createQuestionPost(baseDir: str,
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) -> {}:
"""Question post with multiple choice options
"""
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
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,None,None,subject, \
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")
for questionOption in qOptions:
messageJson['object']['oneof'].append({
"type": "Note",
"name": questionOption,
"replies": {
"type": "Collection",
"totalItems": 0
}
})
return messageJson
2019-07-28 11:08:14 +00:00
def createUnlistedPost(baseDir: str,
2019-10-10 13:12:13 +00:00
nickname: str,domain: str,port: int,httpPrefix: str, \
content: str,followersOnly: bool,saveToFile: bool,
2019-07-28 11:08:14 +00:00
clientToServer: bool,\
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-10-10 13:12:13 +00:00
inReplyTo=None,inReplyToAtomUri=None,subject=None, \
eventDate=None,eventTime=None,location=None) -> {}:
2019-07-28 11:08:14 +00:00
"""Unlisted post. This has the #Public and followers links inverted.
"""
2019-07-28 18:06:20 +00:00
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-10-11 16:16:56 +00:00
return createPostBase(baseDir,nickname,domain,port, \
2019-07-28 18:06:20 +00:00
httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers', \
2019-07-28 11:08:14 +00:00
'https://www.w3.org/ns/activitystreams#Public', \
2019-10-11 16:16:56 +00:00
httpPrefix,content,followersOnly,saveToFile, \
2019-07-28 11:08:14 +00:00
clientToServer, \
2019-08-30 15:50:20 +00:00
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
2019-10-10 13:12:13 +00:00
False,inReplyTo, inReplyToAtomUri, subject, \
eventDate,eventTime,location)
2019-07-28 11:08:14 +00:00
2019-07-27 22:48:34 +00:00
def createFollowersOnlyPost(baseDir: str,
2019-10-10 13:12:13 +00:00
nickname: str,domain: str,port: int,httpPrefix: str, \
content: str,followersOnly: bool,saveToFile: bool,
2019-07-27 22:48:34 +00:00
clientToServer: bool,\
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-10-10 13:12:13 +00:00
inReplyTo=None,inReplyToAtomUri=None,subject=None, \
eventDate=None,eventTime=None,location=None) -> {}:
2019-07-27 22:48:34 +00:00
"""Followers only post
"""
2019-07-28 18:06:20 +00:00
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-10-11 16:16:56 +00:00
return createPostBase(baseDir,nickname,domain,port, \
2019-07-28 18:06:20 +00:00
httpPrefix+'://'+domainFull+'/users/'+nickname+'/followers', \
2019-07-27 22:48:34 +00:00
None,
2019-10-11 16:16:56 +00:00
httpPrefix,content,followersOnly,saveToFile, \
2019-07-27 22:48:34 +00:00
clientToServer, \
2019-08-30 15:50:20 +00:00
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
2019-10-10 13:12:13 +00:00
False,inReplyTo, inReplyToAtomUri, subject, \
eventDate,eventTime,location)
2019-07-27 22:48:34 +00:00
2019-08-19 09:11:25 +00:00
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
mentions=[]
words=content.split(' ')
for wrd in words:
if wrd.startswith('@'):
handle=wrd[1:]
2019-08-19 09:11:25 +00:00
if debug:
print('DEBUG: mentioned handle '+handle)
2019-07-27 22:48:34 +00:00
if '@' not in handle:
handle=handle+'@'+domain
if not os.path.isdir(baseDir+'/accounts/'+handle):
continue
else:
externalDomain=handle.split('@')[1]
if not ('.' in externalDomain or externalDomain=='localhost'):
continue
mentionedNickname=handle.split('@')[0]
2019-08-23 13:47:29 +00:00
mentionedDomain=handle.split('@')[1].strip('\n')
if ':' in mentionedDomain:
mentionedDomain=mentionedDomain.split(':')[0]
if not validNickname(mentionedDomain,mentionedNickname):
2019-07-27 22:48:34 +00:00
continue
actor=httpPrefix+'://'+handle.split('@')[1]+'/users/'+mentionedNickname
mentions.append(actor)
2019-08-19 09:16:33 +00:00
return mentions
2019-07-27 22:48:34 +00:00
def createDirectMessagePost(baseDir: str,
2019-10-10 13:12:13 +00:00
nickname: str,domain: str,port: int,httpPrefix: str, \
content: str,followersOnly: bool,saveToFile: bool,
2019-07-27 22:48:34 +00:00
clientToServer: bool,\
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-10-10 13:12:13 +00:00
inReplyTo=None,inReplyToAtomUri=None,subject=None,debug=False, \
eventDate=None,eventTime=None,location=None) -> {}:
2019-07-27 22:48:34 +00:00
"""Direct Message post
"""
2019-08-19 09:11:25 +00:00
mentionedPeople=getMentionedPeople(baseDir,httpPrefix,content,domain,debug)
if debug:
print('mentionedPeople: '+str(mentionedPeople))
2019-07-27 22:48:34 +00:00
if not mentionedPeople:
return None
2019-08-19 09:37:14 +00:00
postTo=None
postCc=None
messageJson= \
createPostBase(baseDir,nickname,domain,port, \
postTo,postCc, \
httpPrefix,content,followersOnly,saveToFile, \
clientToServer, \
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
False,inReplyTo,inReplyToAtomUri,subject, \
eventDate,eventTime,location)
# mentioned recipients go into To rather than Cc
2019-11-10 12:34:44 +00:00
messageJson['to']=messageJson['object']['cc']
messageJson['object']['to']=messageJson['to']
messageJson['cc']=[]
messageJson['object']['cc']=[]
return messageJson
2019-07-27 22:48:34 +00:00
2019-08-11 11:25:27 +00:00
def createReportPost(baseDir: str,
nickname: str, domain: str, port: int,httpPrefix: str, \
content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool,\
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-08-11 11:25:27 +00:00
debug: bool,subject=None) -> {}:
"""Send a report to moderators
"""
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
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
reportTitle='Moderation Report'
if not subject:
subject=reportTitle
else:
if not subject.startswith(reportTitle):
subject=reportTitle+': '+subject
2019-08-11 13:02:36 +00:00
# create the list of moderators from the moderators file
2019-08-11 11:25:27 +00:00
moderatorsList=[]
moderatorsFile=baseDir+'/accounts/moderators.txt'
if os.path.isfile(moderatorsFile):
with open (moderatorsFile, "r") as fileHandler:
for line in fileHandler:
line=line.strip('\n')
if line.startswith('#'):
continue
if line.startswith('/users/'):
line=line.replace('users','')
if line.startswith('@'):
line=line[1:]
if '@' in line:
moderatorActor=httpPrefix+'://'+domainFull+'/users/'+line.split('@')[0]
if moderatorActor not in moderatorList:
moderatorsList.append(moderatorActor)
continue
if line.startswith('http') or line.startswith('dat'):
# must be a local address - no remote moderators
if '://'+domainFull+'/' in line:
if line not in moderatorsList:
moderatorsList.append(line)
else:
if '/' not in line:
moderatorActor=httpPrefix+'://'+domainFull+'/users/'+line
if moderatorActor not in moderatorsList:
moderatorsList.append(moderatorActor)
if len(moderatorsList)==0:
# if there are no moderators then the admin becomes the moderator
adminNickname=getConfigParam(baseDir,'admin')
if adminNickname:
moderatorsList.append(httpPrefix+'://'+domainFull+'/users/'+adminNickname)
if not moderatorsList:
return None
if debug:
print('DEBUG: Sending report to moderators')
print(str(moderatorsList))
postTo=moderatorsList
postCc=None
2019-08-11 18:32:29 +00:00
postJsonObject=None
for toUrl in postTo:
2019-11-16 22:09:54 +00:00
# who is this report going to?
toNickname=toUrl.split('/users/')[1]
handle=toNickname+'@'+domain
2019-08-11 18:32:29 +00:00
postJsonObject= \
2019-11-16 22:09:54 +00:00
createPostBase(baseDir,nickname,domain,port, \
2019-08-11 18:32:29 +00:00
toUrl,postCc, \
2019-11-16 22:09:54 +00:00
httpPrefix,content,followersOnly,saveToFile, \
2019-08-11 18:32:29 +00:00
clientToServer, \
2019-08-30 15:50:20 +00:00
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
2019-11-16 22:09:54 +00:00
True,None,None,subject, \
2019-10-10 13:12:13 +00:00
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
2019-11-16 22:09:54 +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
newReportFile=baseDir+'/accounts/'+handle+'/.newReport'
if os.path.isfile(newReportFile):
continue
try:
with open(newReportFile, 'w') as fp:
fp.write(toUrl+'/moderation')
except:
pass
2019-08-11 18:32:29 +00:00
return postJsonObject
2019-08-11 11:25:27 +00:00
def threadSendPost(session,postJsonStr: str,federationList: [],\
2019-07-06 13:49:25 +00:00
inboxUrl: str, baseDir: str,signatureHeaderJson: {},postLog: [],
debug :bool) -> None:
"""Sends a with retries
2019-06-30 13:38:01 +00:00
"""
2019-06-30 13:20:23 +00:00
tries=0
sendIntervalSec=30
2019-06-30 13:38:01 +00:00
for attempt in range(20):
2019-10-14 21:05:14 +00:00
postResult=None
2019-10-23 18:44:03 +00:00
unauthorized=False
2019-10-14 21:05:14 +00:00
try:
2019-10-23 18:44:03 +00:00
postResult,unauthorized = \
2019-10-14 21:05:14 +00:00
postJsonString(session,postJsonStr,federationList, \
inboxUrl,signatureHeaderJson, \
"inbox:write",debug)
except Exception as e:
2019-10-23 18:44:03 +00:00
print('ERROR: postJsonString failed '+str(e))
if unauthorized==True:
print(postJsonStr)
print('threadSendPost: Post is unauthorized')
break
2019-08-21 21:05:37 +00:00
if postResult:
logStr='Success on try '+str(tries)+': '+postJsonStr
else:
logStr='Retry '+str(tries)+': '+postJsonStr
postLog.append(logStr)
# keep the length of the log finite
# Don't accumulate massive files on systems with limited resources
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
postLogFilename=baseDir+'/post.log'
with open(postLogFilename, "a+") as logFile:
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:
2019-08-26 12:10:19 +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)
2019-07-06 17:00:22 +00:00
print('DEBUG: json post to '+inboxUrl+' failed. Waiting for '+ \
str(sendIntervalSec)+' seconds.')
time.sleep(sendIntervalSec)
2019-08-21 20:23:20 +00:00
tries+=1
2019-10-16 10:08:21 +00:00
2019-08-14 20:12:27 +00:00
def sendPost(projectVersion: str, \
session,baseDir: str,nickname: str, domain: str, port: int, \
2019-07-03 09:40:27 +00:00
toNickname: str, toDomain: str, toPort: int, cc: str, \
2019-07-03 19:00:03 +00:00
httpPrefix: str, content: str, followersOnly: bool, \
2019-07-06 10:33:57 +00:00
saveToFile: bool, clientToServer: bool, \
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-07-09 14:20:23 +00:00
federationList: [],\
2019-07-03 15:10:18 +00:00
sendThreads: [], postLog: [], cachedWebfingers: {},personCache: {}, \
2019-07-06 13:49:25 +00:00
debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int:
2019-06-30 10:14:02 +00:00
"""Post to another inbox
"""
2019-07-01 09:31:02 +00:00
withDigest=True
if toNickname=='inbox':
# shared inbox actor on @domain@domain
toNickname=toDomain
toDomainOriginal=toDomain
if toPort:
if toPort!=80 and toPort!=443:
if ':' not in toDomain:
toDomain=toDomain+':'+str(toPort)
2019-06-30 22:56:37 +00:00
2019-07-03 19:00:03 +00:00
handle=httpPrefix+'://'+toDomain+'/@'+toNickname
2019-06-30 22:56:37 +00:00
# lookup the inbox for the To handle
2019-08-14 20:12:27 +00:00
wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
domain,projectVersion)
2019-06-30 10:14:02 +00:00
if not wfRequest:
return 1
2019-07-05 22:13:20 +00:00
if not clientToServer:
postToBox='inbox'
else:
postToBox='outbox'
2019-06-30 22:56:37 +00:00
# get the actor inbox for the To handle
inboxUrl,pubKeyId,pubKey,toPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
2019-08-20 09:16:03 +00:00
getPersonBox(baseDir,session,wfRequest,personCache, \
2019-10-17 15:55:05 +00:00
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
2019-07-05 22:13:20 +00:00
if nickname=='capabilities':
inboxUrl=capabilityAcquisition
if not capabilityAcquisition:
return 2
2019-07-05 14:39:24 +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
2019-06-30 10:14:02 +00:00
2019-07-03 15:10:18 +00:00
postJsonObject = \
createPostBase(baseDir,nickname,domain,port, \
2019-07-03 19:00:03 +00:00
toPersonId,cc,httpPrefix,content, \
2019-07-03 15:10:18 +00:00
followersOnly,saveToFile,clientToServer, \
2019-08-30 15:50:20 +00:00
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
2019-10-10 13:12:13 +00:00
False,inReplyTo,inReplyToAtomUri,subject, \
None,None,None)
2019-06-30 10:14:02 +00:00
2019-06-30 22:56:37 +00:00
# get the senders private key
2019-07-03 09:40:27 +00:00
privateKeyPem=getPersonKey(nickname,domain,baseDir,'private')
2019-06-30 10:14:02 +00:00
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
postPath=inboxUrl.split(toDomain,1)[1]
# convert json to string so that there are no
# subsequent conversions after creating message body digest
postJsonStr=json.dumps(postJsonObject)
# construct the http header, including the message body digest
2019-07-02 20:54:22 +00:00
signatureHeaderJson = \
2019-08-16 13:47:01 +00:00
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
2019-11-07 13:35:33 +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')
2019-07-05 18:57:19 +00:00
thr = threadWithTrace(target=threadSendPost,args=(session, \
postJsonStr, \
2019-07-05 18:57:19 +00:00
federationList, \
inboxUrl,baseDir, \
signatureHeaderJson.copy(), \
2019-07-06 13:49:25 +00:00
postLog,
debug),daemon=True)
2019-07-05 18:57:19 +00:00
sendThreads.append(thr)
thr.start()
return 0
2019-08-14 20:12:27 +00:00
def sendPostViaServer(projectVersion: str, \
2019-08-20 09:16:03 +00:00
baseDir: str,session,fromNickname: str,password: str, \
2019-07-16 10:19:04 +00:00
fromDomain: str, fromPort: int, \
toNickname: str, toDomain: str, toPort: int, cc: str, \
httpPrefix: str, content: str, followersOnly: bool, \
2019-08-30 15:50:20 +00:00
attachImageFilename: str,mediaType: str, \
imageDescription: str,useBlurhash: bool, \
2019-07-16 10:19:04 +00:00
cachedWebfingers: {},personCache: {}, \
debug=False,inReplyTo=None,inReplyToAtomUri=None,subject=None) -> int:
"""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
withDigest=True
if toPort:
if toPort!=80 and toPort!=443:
if ':' not in fromDomain:
fromDomain=fromDomain+':'+str(fromPort)
2019-07-16 10:19:04 +00:00
handle=httpPrefix+'://'+fromDomain+'/@'+fromNickname
# lookup the inbox for the To handle
2019-08-14 20:12:27 +00:00
wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
fromDomain,projectVersion)
2019-07-16 10:19:04 +00:00
if not wfRequest:
if debug:
print('DEBUG: webfinger failed for '+handle)
return 1
postToBox='outbox'
# get the actor inbox for the To handle
inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
2019-08-20 09:16:03 +00:00
getPersonBox(baseDir,session,wfRequest,personCache, \
2019-10-17 15:55:05 +00:00
projectVersion,httpPrefix,fromNickname, \
fromDomain,postToBox)
2019-07-16 10:19:04 +00:00
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
# Get the json for the c2s post, not saving anything to file
# Note that baseDir is set to None
saveToFile=False
clientToServer=True
2019-07-17 14:43:51 +00:00
if toDomain.lower().endswith('public'):
toPersonId='https://www.w3.org/ns/activitystreams#Public'
fromDomainFull=fromDomain
if fromPort:
if fromPort!=80 and fromPort!=443:
if ':' not in fromDomain:
fromDomainFull=fromDomain+':'+str(fromPort)
2019-07-17 14:43:51 +00:00
cc=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/followers'
else:
if toDomain.lower().endswith('followers') or \
toDomain.lower().endswith('followersonly'):
toPersonId=httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/followers'
else:
toDomainFull=toDomain
if toPort:
if toPort!=80 and toPort!=443:
if ':' not in toDomain:
toDomainFull=toDomain+':'+str(toPort)
2019-07-17 14:43:51 +00:00
toPersonId=httpPrefix+'://'+toDomainFull+'/users/'+toNickname
2019-07-16 10:19:04 +00:00
postJsonObject = \
2019-08-09 16:24:44 +00:00
createPostBase(baseDir, \
2019-07-16 10:19:04 +00:00
fromNickname,fromDomain,fromPort, \
toPersonId,cc,httpPrefix,content, \
followersOnly,saveToFile,clientToServer, \
2019-08-30 15:50:20 +00:00
attachImageFilename,mediaType, \
imageDescription,useBlurhash, \
2019-10-10 13:12:13 +00:00
False,inReplyTo,inReplyToAtomUri,subject, \
None,None,None)
2019-07-16 14:23:06 +00:00
2019-07-16 10:19:04 +00:00
authHeader=createBasicAuthHeader(fromNickname,password)
2019-07-16 14:23:06 +00:00
if attachImageFilename:
headers = {'host': fromDomain, \
'Authorization': authHeader}
postResult = \
postImage(session,attachImageFilename,[],inboxUrl,headers,"inbox:write")
#if not postResult:
# if debug:
# print('DEBUG: Failed to upload image')
# return 9
2019-07-16 10:19:04 +00:00
headers = {'host': fromDomain, \
2019-11-09 21:39:04 +00:00
'Content-type': 'application/json', \
2019-07-16 10:19:04 +00:00
'Authorization': authHeader}
postResult = \
2019-08-18 11:07:06 +00:00
postJsonString(session,json.dumps(postJsonObject),[],inboxUrl,headers,"inbox:write",debug)
2019-07-16 14:23:06 +00:00
#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
def groupFollowersByDomain(baseDir :str,nickname :str,domain :str) -> {}:
"""Returns a dictionary with followers grouped by domain
"""
handle=nickname+'@'+domain
followersFilename=baseDir+'/accounts/'+handle+'/followers.txt'
if not os.path.isfile(followersFilename):
return None
grouped={}
with open(followersFilename, "r") as f:
for followerHandle in f:
if '@' in followerHandle:
fHandle=followerHandle.strip().replace('\n','')
followerDomain=fHandle.split('@')[1]
if not grouped.get(followerDomain):
grouped[followerDomain]=[fHandle]
else:
grouped[followerDomain].append(fHandle)
return grouped
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
if len(postJsonObject['to'])>1:
return
if len(postJsonObject['to'])==0:
return
if not postJsonObject['to'][0].endswith('#Public'):
return
if postJsonObject.get('cc'):
return
postJsonObject['cc']=postJsonObject['actor']+'/followers'
elif isinstance(postJsonObject['object'], dict):
if not postJsonObject['object'].get('to'):
return
if len(postJsonObject['object']['to'])>1:
return
if len(postJsonObject['object']['to'])==0:
return
if not postJsonObject['object']['to'][0].endswith('#Public'):
return
if postJsonObject['object'].get('cc'):
return
postJsonObject['object']['cc']=postJsonObject['actor']+'/followers'
2019-07-06 17:00:22 +00:00
def sendSignedJson(postJsonObject: {},session,baseDir: str, \
nickname: str, domain: str, port: int, \
2019-07-05 18:57:19 +00:00
toNickname: str, toDomain: str, toPort: int, cc: str, \
2019-07-06 10:33:57 +00:00
httpPrefix: str, saveToFile: bool, clientToServer: bool, \
2019-07-09 14:20:23 +00:00
federationList: [], \
2019-07-06 17:00:22 +00:00
sendThreads: [], postLog: [], cachedWebfingers: {}, \
2019-08-14 20:12:27 +00:00
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
2019-07-05 18:57:19 +00:00
withDigest=True
2019-07-08 13:30:04 +00:00
sharedInbox=False
if toNickname=='inbox':
2019-08-23 13:47:29 +00:00
# shared inbox actor on @domain@domain
toNickname=toDomain
2019-07-08 13:30:04 +00:00
sharedInbox=True
2019-08-16 20:04:24 +00:00
toDomainOriginal=toDomain
2019-08-16 20:04:24 +00:00
if toPort:
if toPort!=80 and toPort!=443:
if ':' not in toDomain:
toDomain=toDomain+':'+str(toPort)
2019-07-05 18:57:19 +00:00
2019-11-07 13:35:33 +00:00
handleBase=httpPrefix+'://'+toDomain+'/@'
2019-10-21 14:12:22 +00:00
if toNickname:
2019-11-07 13:35:33 +00:00
handle=handleBase+toNickname
2019-10-21 14:12:22 +00:00
else:
2019-10-21 15:25:13 +00:00
singleUserInstanceNickname='dev'
2019-11-07 13:35:33 +00:00
handle=handleBase+singleUserInstanceNickname
2019-08-22 20:01:01 +00:00
2019-07-16 22:57:45 +00:00
if debug:
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
wfRequest=webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
domain,projectVersion)
if not wfRequest:
if debug:
print('DEBUG: webfinger for '+handle+' failed')
return 1
2019-07-05 18:57:19 +00:00
2019-10-17 14:41:47 +00:00
if wfRequest.get('errors'):
if debug:
2019-10-17 16:00:34 +00:00
print('DEBUG: webfinger for '+handle+' failed with errors '+str(wfRequest['errors']))
2019-10-17 14:41:47 +00:00
2019-07-05 22:13:20 +00:00
if not clientToServer:
postToBox='inbox'
else:
postToBox='outbox'
2019-07-05 22:13:20 +00:00
# get the actor inbox/outbox/capabilities for the To handle
inboxUrl,pubKeyId,pubKey,toPersonId,sharedInboxUrl,capabilityAcquisition,avatarUrl,displayName = \
2019-08-20 09:16:03 +00:00
getPersonBox(baseDir,session,wfRequest,personCache, \
2019-10-17 15:55:05 +00:00
projectVersion,httpPrefix,nickname,domain,postToBox)
2019-07-05 18:57:19 +00:00
2019-07-05 22:13:20 +00:00
if nickname=='capabilities':
inboxUrl=capabilityAcquisition
if not capabilityAcquisition:
return 2
else:
2019-09-16 11:45:04 +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'):
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:
print('DEBUG: Sending to endpoint '+inboxUrl)
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
2019-07-06 13:49:25 +00:00
privateKeyPem=getPersonKey(nickname,domain,baseDir,'private',debug)
2019-07-05 18:57:19 +00:00
if len(privateKeyPem)==0:
2019-07-06 13:49:25 +00:00
if debug:
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:
2019-08-16 20:04:24 +00:00
print('DEBUG: '+toDomain+' is not in '+inboxUrl)
2019-07-05 22:13:20 +00:00
return 7
2019-08-23 17:03:50 +00:00
postPath=inboxUrl.split(toDomain,1)[1]
2019-10-16 10:58:31 +00:00
addFollowersToPublicPost(postJsonObject)
# convert json to string so that there are no
# subsequent conversions after creating message body digest
2019-11-09 21:39:04 +00:00
postJsonStr=json.dumps(postJsonObject)
# construct the http header, including the message body digest
2019-07-05 18:57:19 +00:00
signatureHeaderJson = \
2019-08-16 13:47:01 +00:00
createSignedHeader(privateKeyPem,nickname,domain,port, \
toDomain,toPort, \
postPath,httpPrefix,withDigest,postJsonStr)
2019-10-16 14:46:29 +00:00
2019-06-30 13:20:23 +00:00
# Keep the number of threads being used small
2019-10-16 18:19:18 +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)
2019-07-06 17:00:22 +00:00
thr = threadWithTrace(target=threadSendPost, \
args=(session, \
postJsonStr, \
2019-07-06 17:00:22 +00:00
federationList, \
inboxUrl,baseDir, \
signatureHeaderJson.copy(), \
postLog,
debug),daemon=True)
2019-06-30 13:20:23 +00:00
sendThreads.append(thr)
2019-10-16 18:19:18 +00:00
#thr.start()
2019-06-30 10:14:02 +00:00
return 0
2019-08-18 09:39:12 +00:00
def addToField(activityType: str,postJsonObject: {},debug: bool) -> ({},bool):
"""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'):
return postJsonObject,True
if debug:
pprint(postJsonObject)
print('DEBUG: no "to" field when sending to named addresses 2')
isSameType=False
toFieldAdded=False
if postJsonObject.get('object'):
if isinstance(postJsonObject['object'], str):
if postJsonObject.get('type'):
if postJsonObject['type']==activityType:
isSameType=True
if debug:
print('DEBUG: "to" field assigned to Follow')
2019-08-18 16:49:35 +00:00
toAddress=postJsonObject['object']
if '/statuses/' in toAddress:
toAddress=toAddress.split('/statuses/')[0]
postJsonObject['to']=[toAddress]
2019-08-18 09:39:12 +00:00
toFieldAdded=True
elif isinstance(postJsonObject['object'], dict):
if postJsonObject['object'].get('type'):
if postJsonObject['object']['type']==activityType:
isSameType=True
if isinstance(postJsonObject['object']['object'], str):
if debug:
print('DEBUG: "to" field assigned to Follow')
2019-08-18 16:49:35 +00:00
toAddress=postJsonObject['object']['object']
if '/statuses/' in toAddress:
toAddress=toAddress.split('/statuses/')[0]
postJsonObject['object']['to']=[toAddress]
2019-08-18 09:39:12 +00:00
postJsonObject['to']=[postJsonObject['object']['object']]
toFieldAdded=True
if not isSameType:
return postJsonObject,True
if toFieldAdded:
return postJsonObject,True
return postJsonObject,False
2019-07-15 18:20:52 +00:00
def sendToNamedAddresses(session,baseDir: str, \
nickname: str, domain: str, port: int, \
httpPrefix: str,federationList: [], \
sendThreads: [],postLog: [], \
cachedWebfingers: {},personCache: {}, \
2019-08-14 20:12:27 +00:00
postJsonObject: {},debug: bool, \
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):
isProfileUpdate=False
# for actor updates there is no 'to' within the object
if postJsonObject['object'].get('type') and postJsonObject.get('type'):
2019-08-22 19:53:24 +00:00
if postJsonObject['type']=='Update' and \
(postJsonObject['object']['type']=='Person' or \
2019-08-23 20:09:00 +00:00
postJsonObject['object']['type']=='Application' or \
2019-10-04 09:23:38 +00:00
postJsonObject['object']['type']=='Group' or \
2019-08-22 19:53:24 +00:00
postJsonObject['object']['type']=='Service'):
2019-08-20 20:35:15 +00:00
# use the original object, which has a 'to'
recipientsObject=postJsonObject
isProfileUpdate=True
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)
print('DEBUG: no "to" field when sending to named addresses')
if postJsonObject['object'].get('type'):
if postJsonObject['object']['type']=='Follow':
if isinstance(postJsonObject['object']['object'], str):
if debug:
print('DEBUG: "to" field assigned to Follow')
postJsonObject['object']['to']=[postJsonObject['object']['object']]
if not postJsonObject['object'].get('to'):
return
recipientsObject=postJsonObject['object']
2019-07-16 19:07:45 +00:00
else:
2019-08-18 09:39:12 +00:00
postJsonObject,fieldAdded=addToField('Follow',postJsonObject,debug)
2019-08-18 16:49:35 +00:00
if not fieldAdded:
return
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
recipientsObject=postJsonObject
2019-07-15 18:20:52 +00:00
recipients=[]
recipientType=['to','cc']
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)
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):
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:
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
2019-07-15 18:20:52 +00:00
clientToServer=False
for address in recipients:
toNickname=getNicknameFromActor(address)
if not toNickname:
continue
toDomain,toPort=getDomainFromActor(address)
if not toDomain:
continue
2019-07-15 18:29:30 +00:00
if debug:
2019-07-16 10:19:04 +00:00
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-07-16 10:19:04 +00:00
toDomainFull=toDomain
if toPort:
if toPort!=80 and toPort!=443:
if ':' not in toDomain:
toDomainFull=toDomain+':'+str(toPort)
print('DEBUG: Post sending s2s: '+nickname+'@'+domainFull+' to '+toNickname+'@'+toDomainFull)
2019-07-16 10:19:04 +00:00
cc=[]
2019-07-15 18:20:52 +00:00
sendSignedJson(postJsonObject,session,baseDir, \
nickname,domain,port, \
toNickname,toDomain,toPort, \
cc,httpPrefix,True,clientToServer, \
federationList, \
sendThreads,postLog,cachedWebfingers, \
2019-08-14 20:12:27 +00:00
personCache,debug,projectVersion)
2019-07-15 18:20:52 +00:00
def hasSharedInbox(session,httpPrefix: str,domain: str) -> bool:
"""Returns true if the given domain has a shared inbox
"""
wfRequest=webfingerHandle(session,domain+'@'+domain,httpPrefix,{}, \
None,__version__)
if wfRequest:
2019-10-17 14:41:47 +00:00
if not wfRequest.get('errors'):
return True
return False
2019-11-04 10:43:19 +00:00
2019-07-15 18:20:52 +00:00
def sendToFollowers(session,baseDir: str, \
nickname: str, domain: str, port: int, \
httpPrefix: str,federationList: [], \
sendThreads: [],postLog: [], \
cachedWebfingers: {},personCache: {}, \
2019-08-14 20:12:27 +00:00
postJsonObject: {},debug: bool, \
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
2019-07-08 13:30:04 +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')
2019-07-08 13:30:04 +00:00
grouped=groupFollowersByDomain(baseDir,nickname,domain)
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
2019-07-15 18:20:52 +00:00
clientToServer=False
2019-07-08 13:30:04 +00:00
# for each instance
for followerDomain,followerHandles in grouped.items():
2019-07-16 22:57:45 +00:00
if debug:
print('DEBUG: follower handles for '+followerDomain)
pprint(followerHandles)
withSharedInbox=hasSharedInbox(session,httpPrefix,followerDomain)
if debug:
2019-08-26 17:44:21 +00:00
if withSharedInbox:
print(followerDomain+' has shared inbox')
else:
print(followerDomain+' does not have a shared inbox')
2019-07-08 13:30:04 +00:00
toPort=port
index=0
toDomain=followerHandles[index].split('@')[1]
if ':' in toDomain:
toPort=toDomain.split(':')[1]
toDomain=toDomain.split(':')[0]
2019-08-22 19:47:10 +00:00
cc=''
2019-11-07 20:51:29 +00:00
2019-11-07 21:12:53 +00:00
if withSharedInbox:
toNickname=followerHandles[index].split('@')[0]
# if there are more than one followers on the domain
# then send the post to the shared inbox
if len(followerHandles)>1:
toNickname='inbox'
2019-11-07 21:12:53 +00:00
if toNickname!='inbox' and postJsonObject.get('type'):
if postJsonObject['type']=='Update':
if postJsonObject.get('object'):
if isinstance(postJsonObject['object'], dict):
if postJsonObject['object'].get('type'):
if postJsonObject['object']['type']=='Person' or \
postJsonObject['object']['type']=='Application' or \
postJsonObject['object']['type']=='Group' or \
postJsonObject['object']['type']=='Service':
print('Sending profile update to shared inbox of '+toDomain)
toNickname='inbox'
if debug:
print('DEBUG: Sending from '+nickname+'@'+domain+' to '+toNickname+'@'+toDomain)
2019-11-04 13:34:43 +00:00
sendSignedJson(postJsonObject,session,baseDir, \
nickname,domain,port, \
toNickname,toDomain,toPort, \
cc,httpPrefix,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:
print('DEBUG: Sending to '+handle)
toNickname=handle.split('@')[0]
2019-11-07 19:23:56 +00:00
2019-11-07 12:56:00 +00:00
if debug:
if postJsonObject['type']!='Update':
print('DEBUG: Sending from '+nickname+'@'+domain+' to '+toNickname+'@'+toDomain)
else:
2019-11-07 19:01:00 +00:00
print('DEBUG: Sending profile update from '+nickname+'@'+domain+' to '+toNickname+'@'+toDomain)
2019-11-07 21:03:17 +00:00
2019-11-07 12:56:00 +00:00
sendSignedJson(postJsonObject,session,baseDir, \
nickname,domain,port, \
toNickname,toDomain,toPort, \
cc,httpPrefix,True,clientToServer, \
federationList, \
sendThreads,postLog,cachedWebfingers, \
personCache,debug,projectVersion)
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')
2019-11-04 10:43:19 +00:00
def sendToFollowersThread(session,baseDir: str, \
nickname: str, domain: str, port: int, \
httpPrefix: str,federationList: [], \
sendThreads: [],postLog: [], \
cachedWebfingers: {},personCache: {}, \
postJsonObject: {},debug: bool, \
projectVersion: str):
"""Returns a thread used to send a post to followers
"""
sendThread= \
threadWithTrace(target=sendToFollowers, \
args=(session,baseDir, \
nickname,domain,port, \
httpPrefix,federationList, \
sendThreads,postLog, \
cachedWebfingers,personCache, \
postJsonObject.copy(),debug, \
projectVersion),daemon=True)
sendThread.start()
return sendThread
2019-07-08 13:30:04 +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, \
2019-11-18 11:28:17 +00:00
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
2019-08-12 13:22:17 +00:00
2019-11-17 14:01:49 +00:00
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, \
2019-11-18 11:28:17 +00:00
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
2019-11-17 14:01:49 +00:00
2019-09-28 16:21:43 +00:00
def createDMTimeline(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
2019-08-25 16:09:56 +00:00
itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
return createBoxIndexed({},session,baseDir,'dm',nickname,domain,port,httpPrefix, \
2019-11-18 11:28:17 +00:00
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
2019-08-25 16:09:56 +00:00
2019-09-28 16:21:43 +00:00
def createRepliesTimeline(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
2019-09-23 19:53:18 +00:00
itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
return createBoxIndexed({},session,baseDir,'tlreplies',nickname,domain,port,httpPrefix, \
2019-11-18 11:28:17 +00:00
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
2019-09-23 19:53:18 +00:00
2019-09-28 16:21:43 +00:00
def createMediaTimeline(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
2019-09-28 11:29:42 +00:00
itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
return createBoxIndexed({},session,baseDir,'tlmedia',nickname,domain,port,httpPrefix, \
2019-11-18 11:28:17 +00:00
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
2019-09-28 11:29:42 +00:00
2019-09-28 16:21:43 +00:00
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, \
2019-11-18 17:04:45 +00:00
itemsPerPage,headerOnly,authorized,False,pageNumber)
2019-07-04 16:24:23 +00:00
2019-08-12 13:22:17 +00:00
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'
if port:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
2019-08-12 13:22:17 +00:00
if not pageNumber:
pageNumber=1
2019-11-16 22:09:54 +00:00
2019-08-12 13:22:17 +00:00
pageStr='?page='+str(pageNumber)
boxHeader = {'@context': 'https://www.w3.org/ns/activitystreams',
'first': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
'last': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
'totalItems': 0,
'type': 'OrderedCollection'}
boxItems = {'@context': 'https://www.w3.org/ns/activitystreams',
'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+pageStr,
'orderedItems': [
],
'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
'type': 'OrderedCollectionPage'}
if isModerator(baseDir,nickname):
moderationIndexFile=baseDir+'/accounts/moderation.txt'
if os.path.isfile(moderationIndexFile):
with open(moderationIndexFile, "r") as f:
lines = f.readlines()
boxHeader['totalItems']=len(lines)
if headerOnly:
return boxHeader
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:
pageLines.append(lines[lineNumber].strip('\n'))
lineNumber-=1
2019-11-16 22:09:54 +00:00
2019-08-12 13:22:17 +00:00
for postUrl in pageLines:
postFilename=boxDir+'/'+postUrl.replace('/','#')+'.json'
if os.path.isfile(postFilename):
2019-10-22 11:55:06 +00:00
postJsonObject=loadJson(postFilename)
if postJsonObject:
boxItems['orderedItems'].append(postJsonObject)
2019-08-12 13:22:17 +00:00
if headerOnly:
return boxHeader
return boxItems
def getStatusNumberFromPostFilename(filename) -> int:
"""Gets the status number from a post filename
eg. https:##testdomain.com:8085#users#testuser567#statuses#1562958506952068.json
returns 156295850695206
"""
if '#statuses#' not in filename:
return None
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
"""
if postJsonObject['type']!='Create':
return False
if not postJsonObject.get('object'):
return False
if not isinstance(postJsonObject['object'], dict):
return False
if postJsonObject['object']['type']!='Note':
return False
2019-11-16 17:29:02 +00:00
if postJsonObject['object'].get('moderationStatus'):
return False
2019-08-25 16:09:56 +00:00
fields=['to','cc']
for f in fields:
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
2019-09-28 16:21:43 +00:00
def isImageMedia(session,baseDir: str,httpPrefix: str,nickname: str,domain: str,postJsonObject: {}) -> bool:
2019-09-28 11:29:42 +00:00
"""Returns true if the given post has attached image media
"""
2019-09-28 12:48:33 +00:00
if postJsonObject['type']=='Announce':
2019-09-28 16:23:02 +00:00
postJsonAnnounce=downloadAnnounce(session,baseDir,httpPrefix,nickname,domain,postJsonObject,__version__)
2019-09-28 16:21:43 +00:00
if postJsonAnnounce:
postJsonObject=postJsonAnnounce
2019-09-28 11:29:42 +00:00
if postJsonObject['type']!='Create':
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
2019-09-28 11:29:42 +00:00
if postJsonObject['object']['type']!='Note':
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
2019-09-23 19:53:18 +00:00
def isReply(postJsonObject: {},actor: str) -> bool:
"""Returns true if the given post is a reply to the given actor
"""
2019-11-09 21:39:04 +00:00
if postJsonObject['type']!='Create':
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
2019-11-09 21:39:04 +00:00
if postJsonObject['object']['type']!='Note':
return False
if postJsonObject['object'].get('inReplyTo'):
if postJsonObject['object']['inReplyTo'].startswith(actor):
return True
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
if tag['type']=='Mention':
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
2019-10-20 09:22:40 +00:00
def createBoxIndex(boxDir: str,postsInBoxDict: {}) -> int:
""" Creates an index for the given box
"""
postsCtr=0
postsInPersonInbox=os.scandir(boxDir)
for postFilename in postsInPersonInbox:
postFilename=postFilename.name
if not postFilename.endswith('.json'):
continue
# extract the status number
statusNumber=getStatusNumberFromPostFilename(postFilename)
if statusNumber:
postsInBoxDict[statusNumber]=os.path.join(boxDir, postFilename)
postsCtr+=1
return postsCtr
2019-10-20 09:49:26 +00:00
def createSharedInboxIndex(baseDir: str,sharedBoxDir: str, \
postsInBoxDict: {},postsCtr: int, \
nickname: str,domain: str, \
ocapAlways: bool) -> int:
2019-10-20 09:36:07 +00:00
""" Creates an index for the given shared inbox
"""
handle=nickname+'@'+domain
followingFilename=baseDir+'/accounts/'+handle+'/following.txt'
postsInSharedInbox=os.scandir(sharedBoxDir)
2019-10-20 09:47:06 +00:00
followingHandles=None
for postFilename in postsInSharedInbox:
2019-10-20 09:36:07 +00:00
postFilename=postFilename.name
if not postFilename.endswith('.json'):
continue
statusNumber=getStatusNumberFromPostFilename(postFilename)
if not statusNumber:
continue
sharedInboxFilename=os.path.join(sharedBoxDir, postFilename)
# get the actor from the shared post
postJsonObject=loadJson(sharedInboxFilename,0)
if not postJsonObject:
print('WARN: json load exception createSharedInboxIndex')
2019-10-20 09:36:07 +00:00
continue
actorNickname=getNicknameFromActor(postJsonObject['actor'])
2019-10-20 09:51:32 +00:00
if not actorNickname:
continue
2019-10-20 09:36:07 +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:
followingHandles = followingFile.read()
if actorNickname+'@'+actorDomain not in followingHandles:
2019-10-20 09:36:07 +00:00
continue
if ocapAlways:
capsList=None
# Note: should this be in the Create or the object of a post?
if postJsonObject.get('capability'):
if isinstance(postJsonObject['capability'], list):
capsList=postJsonObject['capability']
# Have capabilities been granted for the sender?
ocapFilename=baseDir+'/accounts/'+handle+'/ocap/granted/'+postJsonObject['actor'].replace('/','#')+'.json'
if not os.path.isfile(ocapFilename):
continue
# read the capabilities id
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'):
if ocapJson['id'] in capsList:
postsInBoxDict[statusNumber]=sharedInboxFilename
postsCtr+=1
else:
postsInBoxDict[statusNumber]=sharedInboxFilename
postsCtr+=1
return postsCtr
def addPostStringToTimeline(postStr: str,boxname: str,postsInBox: [],boxActor: str) -> bool:
2019-11-18 11:28:17 +00:00
""" is this a valid timeline post?
"""
# must be a "Note" or "Announce" type
if '"Note"' in postStr or '"Announce"' in postStr or \
2019-11-25 10:10:59 +00:00
('"Question"' in postStr and ('"Create"' in postStr or '"Update"' in postStr)):
2019-11-18 11:28:17 +00:00
if boxname=='dm':
if '#Public' in postStr or '/followers' in postStr:
return False
elif boxname=='tlreplies':
if boxActor not in postStr:
return False
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
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:
postStr = postFile.read()
return addPostStringToTimeline(postStr,boxname,postsInBox,boxActor)
2019-11-18 11:28:17 +00:00
return False
def createBoxIndexed(recentPostsCache: {}, \
session,baseDir: str,boxname: str, \
2019-11-18 11:28:17 +00:00
nickname: str,domain: str,port: int,httpPrefix: str, \
itemsPerPage: int,headerOnly: bool,authorized :bool, \
ocapAlways: bool,pageNumber=None) -> {}:
"""Constructs the box feed for a person with the given nickname
"""
if not authorized or not pageNumber:
pageNumber=1
if boxname!='inbox' and boxname!='dm' and \
boxname!='tlreplies' and boxname!='tlmedia' and \
boxname!='outbox' and boxname!='tlbookmarks':
return None
if boxname!='dm' and boxname!='tlreplies' and \
boxname!='tlmedia' and boxname!='tlbookmarks':
boxDir = createPersonDir(nickname,domain,baseDir,boxname)
else:
# extract DMs or replies or media from the inbox
boxDir = createPersonDir(nickname,domain,baseDir,'inbox')
announceCacheDir=baseDir+'/cache/announce/'+nickname
sharedBoxDir=None
if boxname=='inbox' or boxname=='tlreplies' or \
boxname=='tlmedia':
sharedBoxDir = createPersonDir('inbox',domain,baseDir,boxname)
# bookmarks timeline is like the inbox but has its own separate index
indexBoxName=boxname
if boxname=='tlbookmarks':
indexBoxName='bookmarks'
2019-11-18 12:43:21 +00:00
elif boxname=='dm':
indexBoxName='dm'
elif boxname=='tlreplies':
indexBoxName='tlreplies'
elif boxname=='tlmedia':
indexBoxName='tlmedia'
2019-11-18 11:28:17 +00:00
if port:
if port!=80 and port!=443:
if ':' not in domain:
domain=domain+':'+str(port)
boxActor=httpPrefix+'://'+domain+'/users/'+nickname
pageStr='?page=true'
if pageNumber:
try:
pageStr='?page='+str(pageNumber)
except:
pass
boxHeader = {'@context': 'https://www.w3.org/ns/activitystreams',
'first': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
'last': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page=true',
'totalItems': 0,
'type': 'OrderedCollection'}
boxItems = {'@context': 'https://www.w3.org/ns/activitystreams',
'id': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+pageStr,
'orderedItems': [
],
'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname,
'type': 'OrderedCollectionPage'}
postsInBox=[]
indexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+indexBoxName+'.index'
postsCtr=0
if os.path.isfile(indexFilename):
maxPostCtr=itemsPerPage*pageNumber
with open(indexFilename, 'r') as indexFile:
while postsCtr<maxPostCtr:
postFilename=indexFile.readline()
2019-11-18 15:04:08 +00:00
if not postFilename:
postsCtr+=1
continue
# Skip through any posts previous to the current page
if postsCtr<int((pageNumber-1)*itemsPerPage):
2019-11-18 14:12:00 +00:00
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:
postFilename=postFilename.split('/')[-1]
2019-11-18 11:28:17 +00:00
# filename of the post without any extension or path
# This should also correspond to any index entry in the posts cache
2019-11-18 15:04:08 +00:00
postUrl=postFilename.replace('\n','').replace('.json','').strip()
postAdded=False
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):
addPostStringToTimeline(recentPostsCache['json'][postUrl], \
boxname,postsInBox,boxActor)
postAdded=True
if not postAdded:
2019-11-25 10:10:59 +00:00
# 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)
2019-11-18 14:12:00 +00:00
postsCtr+=1
2019-11-18 11:28:17 +00:00
# Generate first and last entries within header
if postsCtr>0:
lastPage=int(postsCtr/itemsPerPage)
if lastPage<1:
lastPage=1
boxHeader['last']= \
httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page='+str(lastPage)
if headerOnly:
2019-11-18 11:55:27 +00:00
boxHeader['totalItems']=len(postsInBox)
2019-11-18 11:28:17 +00:00
prevPageStr='true'
if pageNumber>1:
prevPageStr=str(pageNumber-1)
boxHeader['prev']= \
httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page='+prevPageStr
2019-11-18 11:55:27 +00:00
nextPageStr=str(pageNumber+1)
2019-11-18 11:28:17 +00:00
boxHeader['next']= \
httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page='+nextPageStr
return boxHeader
2019-11-18 11:55:27 +00:00
for postStr in postsInBox:
2019-11-18 11:28:17 +00:00
p=None
try:
p=json.loads(postStr)
except:
continue
# remove any capability so that it's not displayed
if p.get('capability'):
del p['capability']
# Don't show likes, replies or shares (announces) to unauthorized viewers
if not authorized:
if p.get('object'):
if isinstance(p['object'], dict):
if p['object'].get('likes'):
p['likes']={'items': []}
2019-11-18 11:28:17 +00:00
if p['object'].get('replies'):
p['replies']={}
if p['object'].get('shares'):
p['shares']={}
if p['object'].get('bookmarks'):
p['bookmarks']={}
2019-11-18 12:02:55 +00:00
boxItems['orderedItems'].append(p)
2019-11-18 11:28:17 +00:00
return boxItems
2019-10-19 10:23:49 +00:00
def expireCache(baseDir: str,personCache: {},httpPrefix: str,archiveDir: str,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
time.sleep(60*60*24)
expirePersonCache(basedir,personCache)
archivePosts(baseDir,httpPrefix,archiveDir,maxPostsInBox)
2019-10-19 10:23:49 +00:00
def archivePosts(baseDir: str,httpPrefix: str,archiveDir: str,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:
if not os.path.isdir(archiveDir+'/accounts'):
os.mkdir(archiveDir+'/accounts')
for subdir, dirs, files in os.walk(baseDir+'/accounts'):
for handle in dirs:
if '@' in handle:
nickname=handle.split('@')[0]
domain=handle.split('@')[1]
archiveSubdir=None
if archiveDir:
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'
2019-07-14 17:02:41 +00:00
archivePostsForPerson(httpPrefix,nickname,domain,baseDir, \
2019-07-12 20:43:55 +00:00
'inbox',archiveSubdir, \
maxPostsInBox)
if archiveDir:
archiveSubdir=archiveDir+'/accounts/'+handle+'/outbox'
2019-07-14 17:02:41 +00:00
archivePostsForPerson(httpPrefix,nickname,domain,baseDir, \
2019-07-12 20:43:55 +00:00
'outbox',archiveSubdir, \
maxPostsInBox)
2019-07-14 17:02:41 +00:00
def archivePostsForPerson(httpPrefix: str,nickname: str,domain: str,baseDir: str, \
2019-10-19 10:23:49 +00:00
boxname: str,archiveDir: str,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
"""
2019-07-04 16:24:23 +00:00
if boxname!='inbox' and boxname!='outbox':
return
2019-07-12 20:43:55 +00:00
if archiveDir:
if not os.path.isdir(archiveDir):
os.mkdir(archiveDir)
2019-07-04 16:24:23 +00:00
boxDir = createPersonDir(nickname,domain,baseDir,boxname)
2019-09-27 12:09:04 +00:00
postsInBox=os.scandir(boxDir)
2019-10-19 10:19:19 +00:00
noOfPosts=0
for f in postsInBox:
noOfPosts+=1
2019-07-04 16:24:23 +00:00
if noOfPosts<=maxPostsInBox:
2019-06-29 13:44:21 +00:00
return
2019-10-20 11:18:25 +00:00
# remove entries from the index
handle=nickname+'@'+domain
indexFilename=baseDir+'/accounts/'+handle+'/'+boxname+'.index'
if os.path.isfile(indexFilename):
indexCtr=0
# get the existing index entries as a string
newIndex=''
with open(indexFilename, 'r') as indexFile:
for postId in indexFile:
newIndex+=postId
indexCtr+=1
if indexCtr>=maxPostsInBox:
break
# save the new index file
if len(newIndex)>0:
indexFile=open(indexFilename,'w+')
if indexFile:
indexFile.write(newIndex)
indexFile.close()
postsInBoxDict={}
postsCtr=0
2019-11-06 13:49:51 +00:00
postsInBox=os.scandir(boxDir)
for postFilename in postsInBox:
2019-09-27 12:09:04 +00:00
postFilename=postFilename.name
if not postFilename.endswith('.json'):
continue
# Time of file creation
2019-11-06 14:50:17 +00:00
fullFilename=os.path.join(boxDir,postFilename)
if os.path.isfile(fullFilename):
content=open(fullFilename).read()
if '"published":' in content:
publishedStr=content.split('"published":')[1]
if '"' in publishedStr:
publishedStr=publishedStr.split('"')[1]
2019-11-06 14:54:17 +00:00
if publishedStr.endswith('Z'):
postsInBoxDict[publishedStr]=postFilename
postsCtr+=1
noOfPosts=postsCtr
if noOfPosts<=maxPostsInBox:
return
2019-11-06 14:50:17 +00:00
# sort the list in ascending order of date
2019-11-06 14:50:17 +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
postCacheDir=boxDir.replace('/'+boxname,'/postcache')
2019-11-06 14:54:17 +00:00
for publishedStr,postFilename in postsInBoxSorted.items():
2019-11-06 14:50:17 +00:00
filePath=os.path.join(boxDir,postFilename)
2019-09-24 21:16:44 +00:00
if not os.path.isfile(filePath):
continue
if archiveDir:
repliesPath=filePath.replace('.json','.replies')
2019-11-06 14:50:17 +00:00
archivePath=os.path.join(archiveDir,postFilename)
2019-09-24 21:16:44 +00:00
os.rename(filePath,archivePath)
if os.path.isfile(repliesPath):
os.rename(repliesPath,archivePath)
else:
deletePost(baseDir,httpPrefix,nickname,domain,filePath,False)
2019-10-19 10:10:52 +00:00
# remove cached html posts
2019-11-06 14:50:17 +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)
2019-11-06 14:50:17 +00:00
noOfPosts-=1
if noOfPosts<=maxPostsInBox:
2019-09-24 21:16:44 +00:00
break
2019-07-03 10:31:02 +00:00
2019-08-20 09:16:03 +00:00
def getPublicPostsOfPerson(baseDir: str,nickname: str,domain: str, \
2019-07-19 13:32:58 +00:00
raw: bool,simple: bool,useTor: bool, \
2019-07-19 16:56:55 +00:00
port: int,httpPrefix: str, \
2019-08-14 20:12:27 +00:00
debug: bool,projectVersion: str) -> None:
2019-07-03 10:31:02 +00:00
""" This is really just for test purposes
"""
2019-11-13 10:50:16 +00:00
session = createSession(useTor)
2019-07-03 10:31:02 +00:00
personCache={}
cachedWebfingers={}
federationList=[]
2019-07-19 13:32:58 +00:00
domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
domainFull=domain+':'+str(port)
2019-07-19 13:32:58 +00:00
handle=httpPrefix+"://"+domainFull+"/@"+nickname
2019-07-06 17:00:22 +00:00
wfRequest = \
2019-08-14 20:12:27 +00:00
webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
domain,projectVersion)
2019-07-03 10:31:02 +00:00
if not wfRequest:
sys.exit()
personUrl,pubKeyId,pubKey,personId,shaedInbox,capabilityAcquisition,avatarUrl,displayName= \
2019-08-20 09:16:03 +00:00
getPersonBox(baseDir,session,wfRequest,personCache, \
2019-10-17 15:55:05 +00:00
projectVersion,httpPrefix,nickname,domain,'outbox')
2019-10-18 18:57:34 +00:00
wfResult = json.dumps(wfRequest, indent=2, sort_keys=False)
2019-07-03 10:31:02 +00:00
maxMentions=10
maxEmoji=10
maxAttachments=5
2019-07-06 17:00:22 +00:00
userPosts = getPosts(session,personUrl,30,maxMentions,maxEmoji, \
2019-07-09 14:20:23 +00:00
maxAttachments,federationList, \
2019-08-14 20:12:27 +00:00
personCache,raw,simple,debug, \
projectVersion,httpPrefix,domain)
2019-07-03 10:31:02 +00:00
#print(str(userPosts))
2019-07-09 14:20:23 +00:00
def sendCapabilitiesUpdate(session,baseDir: str,httpPrefix: str, \
nickname: str,domain: str,port: int, \
followerUrl,updateCaps: [], \
sendThreads: [],postLog: [], \
cachedWebfingers: {},personCache: {}, \
2019-08-14 20:12:27 +00:00
federationList :[],debug :bool, \
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.
"""
updateJson=capabilitiesUpdate(baseDir,httpPrefix, \
nickname,domain,port, \
followerUrl, \
updateCaps)
if not updateJson:
return 1
if debug:
pprint(updateJson)
print('DEBUG: sending capabilities update from '+ \
nickname+'@'+domain+' port '+ str(port) + \
' to '+followerUrl)
clientToServer=False
followerNickname=getNicknameFromActor(followerUrl)
2019-09-02 09:43:43 +00:00
if not followerNickname:
print('WARN: unable to find nickname in '+followerUrl)
return 1
2019-07-09 14:20:23 +00:00
followerDomain,followerPort=getDomainFromActor(followerUrl)
return sendSignedJson(updateJson,session,baseDir, \
nickname,domain,port, \
followerNickname,followerDomain,followerPort, '', \
httpPrefix,True,clientToServer, \
federationList, \
sendThreads,postLog,cachedWebfingers, \
2019-08-14 20:12:27 +00:00
personCache,debug,projectVersion)
2019-08-02 18:37:23 +00:00
2019-09-28 11:29:42 +00:00
def populateRepliesJson(baseDir: str,nickname: str,domain: str, \
postRepliesFilename: str,authorized: bool, \
repliesJson: {}) -> None:
2019-08-02 18:37:23 +00:00
# populate the items list with replies
repliesBoxes=['outbox','inbox']
with open(postRepliesFilename,'r') as repliesFile:
for messageId in repliesFile:
replyFound=False
# examine inbox and outbox
for boxname in repliesBoxes:
searchFilename= \
baseDir+ \
'/accounts/'+nickname+'@'+ \
domain+'/'+ \
boxname+'/'+ \
messageId.replace('\n','').replace('/','#')+'.json'
if os.path.isfile(searchFilename):
if authorized or \
'https://www.w3.org/ns/activitystreams#Public' in open(searchFilename).read():
2019-10-22 11:55:06 +00:00
postJsonObject=loadJson(searchFilename)
if postJsonObject:
2019-08-02 18:37:23 +00:00
if postJsonObject['object'].get('cc'):
if authorized or \
('https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to'] or \
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['cc']):
repliesJson['orderedItems'].append(postJsonObject)
replyFound=True
else:
if authorized or \
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to']:
repliesJson['orderedItems'].append(postJsonObject)
replyFound=True
break
# if not in either inbox or outbox then examine the shared inbox
if not replyFound:
searchFilename= \
baseDir+ \
'/accounts/inbox@'+ \
domain+'/inbox/'+ \
messageId.replace('\n','').replace('/','#')+'.json'
if os.path.isfile(searchFilename):
if authorized or \
'https://www.w3.org/ns/activitystreams#Public' in open(searchFilename).read():
# get the json of the reply and append it to the collection
2019-10-22 11:55:06 +00:00
postJsonObject=loadJson(searchFilename)
if postJsonObject:
2019-08-02 18:37:23 +00:00
if postJsonObject['object'].get('cc'):
if authorized or \
('https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to'] or \
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['cc']):
repliesJson['orderedItems'].append(postJsonObject)
else:
if authorized or \
'https://www.w3.org/ns/activitystreams#Public' in postJsonObject['object']['to']:
repliesJson['orderedItems'].append(postJsonObject)
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
"""
if not os.path.isfile(announceFilename+'.reject'):
rejectAnnounceFile=open(announceFilename+'.reject', "w+")
rejectAnnounceFile.write('\n')
rejectAnnounceFile.close()
2019-09-28 16:10:45 +00:00
def downloadAnnounce(session,baseDir: str,httpPrefix: str,nickname: str,domain: str,postJsonObject: {},projectVersion: str) -> {}:
"""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
announceCacheDir=baseDir+'/cache/announce/'+nickname
if not os.path.isdir(announceCacheDir):
os.mkdir(announceCacheDir)
announceFilename=announceCacheDir+'/'+postJsonObject['object'].replace('/','#')+'.json'
print('announceFilename: '+announceFilename)
if os.path.isfile(announceFilename+'.reject'):
return None
if os.path.isfile(announceFilename):
print('Reading cached Announce content for '+postJsonObject['object'])
2019-10-22 11:55:06 +00:00
postJsonObject=loadJson(announceFilename)
if postJsonObject:
return postJsonObject
2019-09-28 16:10:45 +00:00
else:
print('Downloading Announce content for '+postJsonObject['object'])
asHeader={'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-10-18 09:28:00 +00:00
if '/channel/' in postJsonObject['actor']:
asHeader={'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
2019-09-28 16:10:45 +00:00
actorNickname=getNicknameFromActor(postJsonObject['actor'])
actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
announcedJson=getJson(session,postJsonObject['object'],asHeader,None,projectVersion,httpPrefix,domain)
if not announcedJson:
return None
if not announcedJson.get('id'):
rejectAnnounce(announceFilename)
2019-10-29 20:23:49 +00:00
#pprint(announcedJson)
2019-09-28 16:10:45 +00:00
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)
2019-10-29 20:23:49 +00:00
#pprint(announcedJson)
2019-09-28 16:10:45 +00:00
return None
if announcedJson['type']!='Note':
rejectAnnounce(announceFilename)
2019-10-29 20:23:49 +00:00
#pprint(announcedJson)
2019-09-28 16:10:45 +00:00
return None
# wrap in create to be consistent with other posts
announcedJson= \
outboxMessageCreateWrap(httpPrefix, \
actorNickname,actorDomain,actorPort, \
announcedJson)
if announcedJson['type']!='Create':
rejectAnnounce(announceFilename)
2019-10-29 20:23:49 +00:00
#pprint(announcedJson)
2019-09-28 16:10:45 +00:00
return None
# set the id to the original status
announcedJson['id']=postJsonObject['object']
announcedJson['object']['id']=postJsonObject['object']
# check that the repeat isn't for a blocked account
attributedNickname=getNicknameFromActor(announcedJson['object']['id'])
attributedDomain,attributedPort=getDomainFromActor(announcedJson['object']['id'])
if attributedNickname and attributedDomain:
if attributedPort:
if attributedPort!=80 and attributedPort!=443:
attributedDomain=attributedDomain+':'+str(attributedPort)
if isBlocked(baseDir,nickname,domain,attributedNickname,attributedDomain):
rejectAnnounce(announceFilename)
return None
postJsonObject=announcedJson
2019-10-22 11:55:06 +00:00
if saveJson(postJsonObject,announceFilename):
return postJsonObject
2019-09-28 16:10:45 +00:00
return None