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
|
2019-06-29 10:08:59 +00:00
|
|
|
import commentjson
|
2019-06-28 18:55:29 +00:00
|
|
|
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
|
2019-07-14 09:17:50 +00:00
|
|
|
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
|
2019-08-17 10:15:01 +00:00
|
|
|
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
|
2019-10-02 14:40:39 +00:00
|
|
|
from utils import locatePost
|
2019-10-22 11:55:06 +00:00
|
|
|
from utils import loadJson
|
|
|
|
from utils import saveJson
|
2019-07-07 16:12:30 +00:00
|
|
|
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'):
|
|
|
|
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']
|
2019-08-22 18:36:07 +00:00
|
|
|
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
|
|
|
|
2019-08-22 18:36: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(''',"'")
|
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-03 22:16:03 +00:00
|
|
|
|
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: {}, \
|
2019-08-01 11:43:22 +00:00
|
|
|
boxname: str) -> str:
|
2019-07-04 16:24:23 +00:00
|
|
|
"""Saves the give json to the give box
|
2019-08-01 11:43:22 +00:00
|
|
|
Returns the filename
|
2019-07-03 22:16:03 +00:00
|
|
|
"""
|
2019-07-04 16:24:23 +00:00
|
|
|
if boxname!='inbox' and boxname!='outbox':
|
2019-08-01 11:43:22 +00:00
|
|
|
return None
|
2019-07-18 11:35:48 +00:00
|
|
|
originalDomain=domain
|
2019-07-03 22:16:03 +00:00
|
|
|
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)
|
2019-08-01 11:43:22 +00:00
|
|
|
return filename
|
2019-07-03 22:16:03 +00:00
|
|
|
|
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()
|
|
|
|
|
2019-07-03 09:40:27 +00:00
|
|
|
def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \
|
2019-07-03 19:00:03 +00:00
|
|
|
toUrl: str, ccUrl: str, httpPrefix: str, content: str, \
|
2019-08-11 20:38:10 +00:00
|
|
|
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, \
|
2019-10-10 13:12:13 +00:00
|
|
|
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
|
|
|
|
2019-08-16 20:35:11 +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-08-09 16:18:00 +00:00
|
|
|
content= \
|
|
|
|
addHtmlTags(baseDir,httpPrefix, \
|
|
|
|
nickname,domain,content, \
|
|
|
|
mentionedRecipients, \
|
|
|
|
hashtagsDict)
|
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-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:
|
|
|
|
if mention not in toRecipients:
|
|
|
|
toRecipients.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)
|
2019-08-10 11:51:54 +00:00
|
|
|
if isPublic:
|
|
|
|
updateHashtagsIndex(baseDir,tag,newPostId)
|
2019-09-29 19:39:35 +00:00
|
|
|
print('Content tags: '+str(tags))
|
2019-09-29 17:42:51 +00:00
|
|
|
content=replaceEmojiFromTags(content,tags,'content')
|
2019-10-02 14:40:39 +00:00
|
|
|
|
|
|
|
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)
|
2019-10-02 14:40:39 +00:00
|
|
|
if replyPostFilename:
|
2019-10-22 11:55:06 +00:00
|
|
|
replyToJson=loadJson(replyPostFilename)
|
2019-10-02 14:40:39 +00:00
|
|
|
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,
|
|
|
|
'to': [toUrl],
|
|
|
|
'cc': [],
|
|
|
|
'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-07-03 15:10:18 +00:00
|
|
|
'cc': [],
|
|
|
|
'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-07-03 15:10:18 +00:00
|
|
|
'cc': [],
|
|
|
|
'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
|
|
|
|
"""
|
|
|
|
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
|
|
|
"""
|
2019-08-16 20:35:11 +00:00
|
|
|
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-07-16 19:07:45 +00:00
|
|
|
if not postJsonObject['object'].get('to'):
|
|
|
|
return False
|
|
|
|
toList=postJsonObject['object']['to']
|
|
|
|
if postJsonObject['object'].get('cc'):
|
|
|
|
ccList=postJsonObject['object']['cc']
|
|
|
|
else:
|
|
|
|
if not postJsonObject.get('to'):
|
|
|
|
return False
|
|
|
|
toList=postJsonObject['to']
|
|
|
|
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
|
|
|
|
if not addressedToFollowers:
|
2019-07-16 19:07:45 +00:00
|
|
|
if followersUrl in ccList:
|
2019-07-08 13:30:04 +00:00
|
|
|
addressedToFollowers=True
|
|
|
|
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
|
2019-08-16 20:35:11 +00:00
|
|
|
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-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
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
2019-10-11 16:16:56 +00:00
|
|
|
return createPostBase(baseDir,nickname,domain,port, \
|
2019-07-27 22:48:34 +00:00
|
|
|
postTo,postCc, \
|
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-11 16:16:56 +00:00
|
|
|
False,inReplyTo,inReplyToAtomUri,subject, \
|
2019-10-10 13:12:13 +00:00
|
|
|
eventDate,eventTime,location)
|
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:
|
2019-08-16 20:35:11 +00:00
|
|
|
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:
|
|
|
|
postJsonObject= \
|
|
|
|
createPostBase(baseDir,nickname, domain, port, \
|
|
|
|
toUrl,postCc, \
|
|
|
|
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
|
|
|
True,None, None, subject, \
|
|
|
|
None,None,None)
|
2019-08-11 18:32:29 +00:00
|
|
|
return postJsonObject
|
2019-08-11 11:25:27 +00:00
|
|
|
|
2019-08-17 10:15:01 +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:
|
2019-06-30 13:38:01 +00:00
|
|
|
"""Sends a post with exponential backoff
|
|
|
|
"""
|
2019-06-30 13:20:23 +00:00
|
|
|
tries=0
|
2019-06-30 13:38:01 +00:00
|
|
|
backoffTime=60
|
|
|
|
for attempt in range(20):
|
2019-10-14 21:05:14 +00:00
|
|
|
postResult=None
|
|
|
|
try:
|
|
|
|
postResult = \
|
|
|
|
postJsonString(session,postJsonStr,federationList, \
|
|
|
|
inboxUrl,signatureHeaderJson, \
|
|
|
|
"inbox:write",debug)
|
|
|
|
except Exception as e:
|
|
|
|
print('ERROR: postJsonString failed')
|
|
|
|
print(e)
|
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(backoffTime)+' seconds.')
|
2019-06-30 13:20:23 +00:00
|
|
|
time.sleep(backoffTime)
|
|
|
|
backoffTime *= 2
|
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
|
|
|
|
|
2019-08-23 14:08:10 +00:00
|
|
|
if toNickname=='inbox':
|
|
|
|
# shared inbox actor on @domain@domain
|
|
|
|
toNickname=toDomain
|
|
|
|
|
|
|
|
toDomainOriginal=toDomain
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
2019-08-22 18:36:07 +00:00
|
|
|
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
|
2019-08-23 14:08:10 +00:00
|
|
|
postPath=inboxUrl.split(toDomain,1)[1]
|
2019-08-17 10:15:01 +00:00
|
|
|
|
|
|
|
# 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, \
|
2019-08-17 10:15:01 +00:00
|
|
|
postPath,httpPrefix,withDigest,postJsonStr)
|
2019-07-05 18:57:19 +00:00
|
|
|
|
|
|
|
# Keep the number of threads being used small
|
2019-10-16 10:08:21 +00:00
|
|
|
while len(sendThreads)>20:
|
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, \
|
2019-08-17 10:15:01 +00:00
|
|
|
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
|
|
|
|
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
2019-08-22 18:36:07 +00:00
|
|
|
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:
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
2019-08-16 20:35:11 +00:00
|
|
|
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, \
|
|
|
|
'Content-type': 'application/json', \
|
|
|
|
'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
|
|
|
|
|
2019-07-08 08:51:33 +00:00
|
|
|
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
|
|
|
|
2019-08-23 14:08:10 +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-10-21 14:12:22 +00:00
|
|
|
if toNickname:
|
|
|
|
handle=httpPrefix+'://'+toDomain+'/@'+toNickname
|
|
|
|
else:
|
2019-10-21 15:25:13 +00:00
|
|
|
singleUserInstanceNickname='dev'
|
|
|
|
handle=httpPrefix+'://'+toDomain+'/@'+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-08-22 20:26:57 +00:00
|
|
|
|
2019-07-05 22:13:20 +00:00
|
|
|
# get the actor inbox/outbox/capabilities for the To handle
|
2019-08-22 18:36:07 +00:00
|
|
|
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-08-17 10:15:01 +00:00
|
|
|
|
2019-10-16 10:58:31 +00:00
|
|
|
addFollowersToPublicPost(postJsonObject)
|
|
|
|
|
2019-08-17 10:15:01 +00:00
|
|
|
# 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-05 18:57:19 +00:00
|
|
|
signatureHeaderJson = \
|
2019-08-16 13:47:01 +00:00
|
|
|
createSignedHeader(privateKeyPem,nickname,domain,port, \
|
|
|
|
toDomain,toPort, \
|
2019-08-17 10:15:01 +00:00
|
|
|
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, \
|
2019-08-17 10:15:01 +00:00
|
|
|
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)
|
2019-08-18 21:12:37 +00:00
|
|
|
print('recipientsObject: '+str(recipientsObject[rType]))
|
2019-08-18 20:54:33 +00:00
|
|
|
for address in recipientsObject[rType]:
|
2019-08-18 21:15:09 +00:00
|
|
|
if not address:
|
|
|
|
continue
|
2019-08-18 21:12:37 +00:00
|
|
|
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:
|
2019-08-05 21:14:38 +00:00
|
|
|
print('DEBUG: Sending individually addressed posts: '+str(recipients))
|
2019-07-15 18:29:30 +00:00
|
|
|
# this is after the message has arrived at the server
|
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:
|
2019-08-16 20:35:11 +00:00
|
|
|
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:
|
2019-08-16 20:35:11 +00:00
|
|
|
if ':' not in toDomain:
|
|
|
|
toDomainFull=toDomain+':'+str(toPort)
|
2019-08-05 21:14:38 +00:00
|
|
|
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
|
|
|
|
2019-08-26 17:42:06 +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
|
2019-08-26 17:42:06 +00:00
|
|
|
return False
|
|
|
|
|
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-08-26 17:42:06 +00:00
|
|
|
|
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)
|
2019-08-26 17:42:06 +00:00
|
|
|
withSharedInbox=hasSharedInbox(session,httpPrefix,followerDomain)
|
|
|
|
if debug:
|
2019-08-26 17:44:21 +00:00
|
|
|
if withSharedInbox:
|
2019-08-26 17:42:06 +00:00
|
|
|
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
|
|
|
|
|
|
|
# If this is a profile update then send to shared inbox
|
2019-08-26 17:42:06 +00:00
|
|
|
cc=''
|
|
|
|
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'
|
|
|
|
|
|
|
|
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 \
|
2019-10-04 09:23:38 +00:00
|
|
|
postJsonObject['object']['type']=='Group' or \
|
2019-08-26 17:42:06 +00:00
|
|
|
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)
|
|
|
|
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:
|
|
|
|
toNickname=handle.split('@')[0]
|
2019-08-22 19:47:10 +00:00
|
|
|
|
2019-08-26 17:42:06 +00:00
|
|
|
if debug:
|
|
|
|
print('DEBUG: Sending from '+nickname+'@'+domain+' to '+toNickname+'@'+toDomain)
|
|
|
|
sendSignedJson(postJsonObject,session,baseDir, \
|
|
|
|
nickname,domain,port, \
|
|
|
|
toNickname,toDomain,toPort, \
|
|
|
|
cc,httpPrefix,True,clientToServer, \
|
|
|
|
federationList, \
|
|
|
|
sendThreads,postLog,cachedWebfingers, \
|
|
|
|
personCache,debug,projectVersion)
|
|
|
|
|
2019-07-16 22:57:45 +00:00
|
|
|
if debug:
|
|
|
|
print('DEBUG: End of sendToFollowers')
|
|
|
|
|
2019-07-08 13:30:04 +00:00
|
|
|
|
2019-09-28 16:21:43 +00:00
|
|
|
def createInbox(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
|
2019-07-14 11:15:28 +00:00
|
|
|
itemsPerPage: int,headerOnly: bool,ocapAlways: bool,pageNumber=None) -> {}:
|
2019-09-28 16:21:43 +00:00
|
|
|
return createBoxBase(session,baseDir,'inbox',nickname,domain,port,httpPrefix, \
|
2019-07-14 11:15:28 +00:00
|
|
|
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
|
2019-08-12 13:22:17 +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) -> {}:
|
2019-09-28 16:21:43 +00:00
|
|
|
return createBoxBase(session,baseDir,'dm',nickname,domain,port,httpPrefix, \
|
2019-08-25 16:09:56 +00:00
|
|
|
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
|
|
|
|
|
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) -> {}:
|
2019-09-28 16:21:43 +00:00
|
|
|
return createBoxBase(session,baseDir,'tlreplies',nickname,domain,port,httpPrefix, \
|
2019-09-23 19:53:18 +00:00
|
|
|
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
|
|
|
|
|
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) -> {}:
|
2019-09-28 16:21:43 +00:00
|
|
|
return createBoxBase(session,baseDir,'tlmedia',nickname,domain,port,httpPrefix, \
|
2019-09-28 11:29:42 +00:00
|
|
|
itemsPerPage,headerOnly,True,ocapAlways,pageNumber)
|
|
|
|
|
2019-09-28 16:21:43 +00:00
|
|
|
def createOutbox(session,baseDir: str,nickname: str,domain: str,port: int,httpPrefix: str, \
|
2019-07-12 11:20:59 +00:00
|
|
|
itemsPerPage: int,headerOnly: bool,authorized: bool,pageNumber=None) -> {}:
|
2019-09-28 16:21:43 +00:00
|
|
|
return createBoxBase(session,baseDir,'outbox',nickname,domain,port,httpPrefix, \
|
2019-07-14 11:15:28 +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'
|
|
|
|
|
2019-08-16 20:35:11 +00:00
|
|
|
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
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
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-09-17 12:14:36 +00:00
|
|
|
|
2019-08-12 13:22:17 +00:00
|
|
|
if headerOnly:
|
|
|
|
return boxHeader
|
|
|
|
return boxItems
|
|
|
|
|
2019-07-14 09:17:50 +00:00
|
|
|
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
|
|
|
|
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
|
|
|
|
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'):
|
2019-09-28 12:27:48 +00:00
|
|
|
if attach['mediaType'].startswith('image/'):
|
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
|
|
|
|
"""
|
|
|
|
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-10-22 19:07:23 +00:00
|
|
|
if postJsonObject['object'].get('inReplyTo'):
|
|
|
|
if postJsonObject['object']['inReplyTo'].startswith(actor):
|
|
|
|
return True
|
|
|
|
if not postJsonObject['object'].get('tag'):
|
2019-09-23 19:53:18 +00:00
|
|
|
return False
|
2019-10-22 19:07:23 +00:00
|
|
|
if not isinstance(postJsonObject['object']['tag'], list):
|
2019-09-23 19:53:18 +00:00
|
|
|
return False
|
2019-10-22 19:07:23 +00:00
|
|
|
for tag in postJsonObject['object']['tag']:
|
|
|
|
if not tag.get('type'):
|
|
|
|
continue
|
|
|
|
if tag['type']=='Mention':
|
|
|
|
if not tag.get('href'):
|
|
|
|
continue
|
|
|
|
if actor in tag['href']:
|
|
|
|
return True
|
|
|
|
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
|
2019-10-20 09:53:25 +00:00
|
|
|
try:
|
|
|
|
with open(sharedInboxFilename, 'r') as fp:
|
|
|
|
postJsonObject=commentjson.load(fp)
|
|
|
|
except Exception as e:
|
|
|
|
print('WARN: commentjson exception createBoxBase - '+str(e))
|
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
|
|
|
|
loadedOcap=False
|
|
|
|
try:
|
|
|
|
with open(ocapFilename, 'r') as fp:
|
|
|
|
ocapJson=commentjson.load(fp)
|
|
|
|
loadedOcap=True
|
|
|
|
except Exception as e:
|
|
|
|
print('WARN: commentjson exception createBoxBase - '+str(e))
|
|
|
|
|
|
|
|
if loadedOcap:
|
|
|
|
if ocapJson.get('id'):
|
|
|
|
if ocapJson['id'] in capsList:
|
|
|
|
postsInBoxDict[statusNumber]=sharedInboxFilename
|
|
|
|
postsCtr+=1
|
|
|
|
else:
|
|
|
|
postsInBoxDict[statusNumber]=sharedInboxFilename
|
|
|
|
postsCtr+=1
|
|
|
|
return postsCtr
|
|
|
|
|
2019-09-28 16:21:43 +00:00
|
|
|
def createBoxBase(session,baseDir: str,boxname: str, \
|
2019-07-06 17:00:22 +00:00
|
|
|
nickname: str,domain: str,port: int,httpPrefix: str, \
|
2019-07-14 11:15:28 +00:00
|
|
|
itemsPerPage: int,headerOnly: bool,authorized :bool, \
|
|
|
|
ocapAlways: bool,pageNumber=None) -> {}:
|
2019-07-14 09:17:50 +00:00
|
|
|
"""Constructs the box feed for a person with the given nickname
|
2019-06-29 13:17:02 +00:00
|
|
|
"""
|
2019-10-22 13:53:51 +00:00
|
|
|
if not authorized:
|
|
|
|
pageNumber=1
|
|
|
|
|
2019-09-28 11:29:42 +00:00
|
|
|
if boxname!='inbox' and boxname!='dm' and \
|
|
|
|
boxname!='tlreplies' and boxname!='tlmedia' and boxname!='outbox':
|
2019-07-04 16:24:23 +00:00
|
|
|
return None
|
2019-09-28 11:29:42 +00:00
|
|
|
if boxname!='dm' and boxname!='tlreplies' and boxname!='tlmedia':
|
2019-08-25 16:09:56 +00:00
|
|
|
boxDir = createPersonDir(nickname,domain,baseDir,boxname)
|
|
|
|
else:
|
2019-09-28 12:27:48 +00:00
|
|
|
# extract DMs or replies or media from the inbox
|
2019-08-25 16:09:56 +00:00
|
|
|
boxDir = createPersonDir(nickname,domain,baseDir,'inbox')
|
2019-07-10 17:03:51 +00:00
|
|
|
sharedBoxDir=None
|
2019-09-28 11:29:42 +00:00
|
|
|
if boxname=='inbox' or boxname=='tlreplies' or boxname=='tlmedia':
|
2019-07-10 17:03:51 +00:00
|
|
|
sharedBoxDir = createPersonDir('inbox',domain,baseDir,boxname)
|
2019-06-30 19:01:43 +00:00
|
|
|
|
2019-08-16 20:35:11 +00:00
|
|
|
if port:
|
|
|
|
if port!=80 and port!=443:
|
|
|
|
if ':' not in domain:
|
|
|
|
domain=domain+':'+str(port)
|
2019-09-23 19:53:18 +00:00
|
|
|
|
|
|
|
boxActor=httpPrefix+'://'+domain+'/users/'+nickname
|
|
|
|
|
2019-06-29 16:47:37 +00:00
|
|
|
pageStr='?page=true'
|
|
|
|
if pageNumber:
|
|
|
|
try:
|
|
|
|
pageStr='?page='+str(pageNumber)
|
|
|
|
except:
|
|
|
|
pass
|
2019-07-04 16:24:23 +00:00
|
|
|
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'}
|
2019-06-29 13:17:02 +00:00
|
|
|
|
2019-08-20 11:10:53 +00:00
|
|
|
# counter for posts so far added to the target page
|
2019-06-29 16:47:37 +00:00
|
|
|
postsOnPageCtr=0
|
2019-06-29 13:17:02 +00:00
|
|
|
|
|
|
|
# post filenames sorted in descending order
|
2019-07-14 09:17:50 +00:00
|
|
|
postsInBoxDict={}
|
2019-10-20 11:42:46 +00:00
|
|
|
postsInBox={}
|
2019-07-10 17:03:51 +00:00
|
|
|
|
2019-10-20 11:40:35 +00:00
|
|
|
indexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+boxname+'.index'
|
2019-10-23 10:43:20 +00:00
|
|
|
lookedUpFromIndex=False
|
2019-10-20 11:40:35 +00:00
|
|
|
if os.path.isfile(indexFilename):
|
|
|
|
print('DEBUG: using index file to construct timeline')
|
|
|
|
postsCtr=0
|
|
|
|
maxPostCtr=None
|
2019-10-20 14:14:21 +00:00
|
|
|
if not pageNumber:
|
2019-10-20 14:28:47 +00:00
|
|
|
minPageNumber=4
|
2019-10-20 14:14:21 +00:00
|
|
|
else:
|
2019-10-20 14:28:47 +00:00
|
|
|
minPageNumber=pageNumber+4
|
|
|
|
maxPostCtr=itemsPerPage*minPageNumber
|
2019-10-20 11:40:35 +00:00
|
|
|
with open(indexFilename, 'r') as indexFile:
|
2019-10-22 21:53:15 +00:00
|
|
|
while postsCtr<maxPostCtr:
|
|
|
|
postFilename=indexFile.readline()
|
|
|
|
if not postFilename:
|
|
|
|
break
|
2019-10-20 12:43:59 +00:00
|
|
|
postsInBox[postsCtr]=os.path.join(boxDir, postFilename.replace('\n',''))
|
2019-10-20 11:40:35 +00:00
|
|
|
postsCtr+=1
|
2019-10-23 10:43:20 +00:00
|
|
|
lookedUpFromIndex=True
|
2019-10-20 11:40:35 +00:00
|
|
|
else:
|
|
|
|
postsCtr=createBoxIndex(boxDir,postsInBoxDict)
|
|
|
|
|
|
|
|
# combine the inbox for the account with the shared inbox
|
|
|
|
if sharedBoxDir:
|
|
|
|
postsCtr= \
|
|
|
|
createSharedInboxIndex(baseDir,sharedBoxDir, \
|
|
|
|
postsInBoxDict,postsCtr, \
|
|
|
|
nickname,domain,ocapAlways)
|
2019-07-14 09:17:50 +00:00
|
|
|
|
2019-10-20 11:40:35 +00:00
|
|
|
# sort the list in descending order of date
|
|
|
|
postsInBox=OrderedDict(sorted(postsInBoxDict.items(),reverse=True))
|
2019-06-29 13:17:02 +00:00
|
|
|
|
2019-07-04 16:24:23 +00:00
|
|
|
# number of posts in box
|
2019-07-14 09:17:50 +00:00
|
|
|
boxHeader['totalItems']=postsCtr
|
2019-06-29 13:17:02 +00:00
|
|
|
prevPostFilename=None
|
|
|
|
|
2019-06-29 17:07:43 +00:00
|
|
|
if not pageNumber:
|
|
|
|
pageNumber=1
|
|
|
|
|
2019-06-29 13:17:02 +00:00
|
|
|
# Generate first and last entries within header
|
2019-07-14 09:17:50 +00:00
|
|
|
if postsCtr>0:
|
|
|
|
lastPage=int(postsCtr/itemsPerPage)
|
2019-06-29 17:07:43 +00:00
|
|
|
if lastPage<1:
|
|
|
|
lastPage=1
|
2019-07-04 16:24:23 +00:00
|
|
|
boxHeader['last']= \
|
|
|
|
httpPrefix+'://'+domain+'/users/'+nickname+'/'+boxname+'?page='+str(lastPage)
|
2019-06-29 13:17:02 +00:00
|
|
|
|
|
|
|
# Insert posts
|
2019-06-29 16:47:37 +00:00
|
|
|
currPage=1
|
|
|
|
postsCtr=0
|
2019-07-14 09:17:50 +00:00
|
|
|
for statusNumber,postFilename in postsInBox.items():
|
2019-06-29 16:47:37 +00:00
|
|
|
# Are we at the starting page yet?
|
|
|
|
if prevPostFilename and currPage==pageNumber and postsCtr==0:
|
|
|
|
# update the prev entry for the last message id
|
2019-10-20 11:59:47 +00:00
|
|
|
if '#statuses#' in prevPostFilename:
|
|
|
|
postId = prevPostFilename.split('#statuses#')[1].replace('#activity','')
|
|
|
|
boxHeader['prev']= \
|
|
|
|
httpPrefix+'://'+domain+'/users/'+nickname+'/'+ \
|
|
|
|
boxname+'?min_id='+postId+'&page=true'
|
|
|
|
|
2019-06-29 13:23:46 +00:00
|
|
|
# get the full path of the post file
|
2019-07-10 17:03:51 +00:00
|
|
|
filePath = postFilename
|
2019-08-20 13:47:39 +00:00
|
|
|
try:
|
|
|
|
if os.path.isfile(filePath):
|
2019-08-25 17:12:48 +00:00
|
|
|
# is this a valid timeline post?
|
|
|
|
isPost=False
|
|
|
|
# must be a "Note" or "Announce" type
|
|
|
|
with open(filePath, 'r') as file:
|
|
|
|
postStr = file.read()
|
2019-09-06 15:08:32 +00:00
|
|
|
if '"Note"' in postStr or '"Announce"' in postStr or \
|
|
|
|
('"Question"' in postStr and '"Create"' in postStr):
|
2019-08-25 17:12:48 +00:00
|
|
|
isPost=True
|
|
|
|
if boxname=='dm':
|
|
|
|
if '#Public' in postStr or '/followers' in postStr:
|
|
|
|
isPost=False
|
2019-09-23 20:43:18 +00:00
|
|
|
elif boxname=='tlreplies':
|
2019-09-23 20:16:36 +00:00
|
|
|
if boxActor not in postStr:
|
2019-09-23 20:15:21 +00:00
|
|
|
isPost=False
|
2019-09-28 11:40:42 +00:00
|
|
|
elif boxname=='tlmedia':
|
2019-09-28 13:22:18 +00:00
|
|
|
if '"Create"' in postStr:
|
|
|
|
if 'mediaType' not in postStr or 'image/' not in postStr:
|
|
|
|
isPost=False
|
2019-08-25 17:12:48 +00:00
|
|
|
|
|
|
|
if isPost:
|
2019-09-24 21:56:53 +00:00
|
|
|
isTimelinePost=False
|
2019-09-24 22:24:45 +00:00
|
|
|
if currPage <= pageNumber and postsOnPageCtr <= itemsPerPage:
|
2019-08-25 17:12:48 +00:00
|
|
|
# get the post as json
|
|
|
|
p = json.loads(postStr)
|
2019-08-25 16:09:56 +00:00
|
|
|
|
2019-10-23 10:43:20 +00:00
|
|
|
if lookedUpFromIndex:
|
2019-09-23 20:31:49 +00:00
|
|
|
isTimelinePost=True
|
|
|
|
else:
|
2019-10-23 10:43:20 +00:00
|
|
|
if (boxname!='dm' and boxname!='tlreplies' and boxname!='tlmedia'):
|
|
|
|
isTimelinePost=True
|
|
|
|
else:
|
|
|
|
if boxname=='dm':
|
|
|
|
if isDM(p):
|
|
|
|
isTimelinePost=True
|
|
|
|
elif boxname=='tlreplies':
|
|
|
|
if isDM(p) or isReply(p,boxActor):
|
|
|
|
isTimelinePost=True
|
|
|
|
elif boxname=='tlmedia':
|
|
|
|
if isImageMedia(session,baseDir,httpPrefix,nickname,domain,p):
|
|
|
|
isTimelinePost=True
|
2019-09-24 22:24:45 +00:00
|
|
|
|
|
|
|
if isTimelinePost and currPage == pageNumber:
|
2019-08-25 16:09:56 +00:00
|
|
|
# remove any capability so that it's not displayed
|
|
|
|
if p.get('capability'):
|
|
|
|
del p['capability']
|
|
|
|
# Don't show likes or replies to unauthorized viewers
|
|
|
|
if not authorized:
|
|
|
|
if p.get('object'):
|
|
|
|
if isinstance(p['object'], dict):
|
|
|
|
if p['object'].get('likes'):
|
|
|
|
p['likes']={}
|
|
|
|
if p['object'].get('replies'):
|
|
|
|
p['replies']={}
|
|
|
|
# insert it into the box feed
|
|
|
|
if postsOnPageCtr < itemsPerPage:
|
|
|
|
if not headerOnly:
|
|
|
|
boxItems['orderedItems'].append(p)
|
|
|
|
postsOnPageCtr += 1
|
2019-09-28 19:48:29 +00:00
|
|
|
if postsOnPageCtr >= itemsPerPage:
|
2019-08-25 16:09:56 +00:00
|
|
|
# if this is the last post update the next message ID
|
|
|
|
if '/statuses/' in p['id']:
|
|
|
|
postId = p['id'].split('/statuses/')[1].replace('/activity','')
|
|
|
|
boxHeader['next']= \
|
|
|
|
httpPrefix+'://'+domain+'/users/'+ \
|
|
|
|
nickname+'/'+boxname+'?max_id='+ \
|
|
|
|
postId+'&page=true'
|
2019-08-25 17:12:48 +00:00
|
|
|
|
|
|
|
# remember the last post filename for use with prev
|
|
|
|
prevPostFilename = postFilename
|
|
|
|
if postsOnPageCtr >= itemsPerPage:
|
|
|
|
break
|
|
|
|
# count the pages
|
2019-09-24 22:24:45 +00:00
|
|
|
if isTimelinePost:
|
2019-09-24 21:56:53 +00:00
|
|
|
postsCtr += 1
|
|
|
|
if postsCtr >= itemsPerPage:
|
|
|
|
postsCtr = 0
|
|
|
|
currPage += 1
|
2019-08-20 13:47:39 +00:00
|
|
|
except Exception as e:
|
|
|
|
print(e)
|
2019-06-29 16:47:37 +00:00
|
|
|
if headerOnly:
|
2019-07-04 16:24:23 +00:00
|
|
|
return boxHeader
|
|
|
|
return boxItems
|
2019-06-29 13:44:21 +00:00
|
|
|
|
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-09-14 11:18:34 +00:00
|
|
|
|
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()
|
|
|
|
|
2019-09-14 11:18:34 +00:00
|
|
|
postsInBoxDict={}
|
|
|
|
postsCtr=0
|
|
|
|
for postFilename in postsInBox:
|
2019-09-27 12:09:04 +00:00
|
|
|
postFilename=postFilename.name
|
2019-09-14 11:18:34 +00:00
|
|
|
if not postFilename.endswith('.json'):
|
|
|
|
continue
|
|
|
|
# extract the status number
|
|
|
|
statusNumber=getStatusNumberFromPostFilename(postFilename)
|
|
|
|
if statusNumber:
|
|
|
|
postsInBoxDict[statusNumber]=os.path.join(boxDir, postFilename)
|
|
|
|
postsCtr+=1
|
|
|
|
|
|
|
|
noOfPosts=postsCtr
|
|
|
|
if noOfPosts<=maxPostsInBox:
|
|
|
|
return
|
|
|
|
# sort the list in ascending order of date
|
|
|
|
postsInBox=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-09-14 11:21:59 +00:00
|
|
|
for statusNumber,postFilename in postsInBox.items():
|
2019-07-14 15:43:02 +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')
|
|
|
|
archivePath = os.path.join(archiveDir, postFilename)
|
|
|
|
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
|
|
|
|
postCacheFilename=os.path.join(postCacheDir, postFilename).replace('.json','.html')
|
|
|
|
if os.path.isfile(postCacheFilename):
|
|
|
|
os.remove(postCacheFilename)
|
|
|
|
|
2019-09-24 21:16:44 +00:00
|
|
|
noOfPosts -= 1
|
|
|
|
if noOfPosts <= maxPostsInBox:
|
|
|
|
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
|
|
|
|
"""
|
|
|
|
session = createSession(domain,port,useTor)
|
|
|
|
personCache={}
|
|
|
|
cachedWebfingers={}
|
|
|
|
federationList=[]
|
|
|
|
|
2019-07-19 13:32:58 +00:00
|
|
|
domainFull=domain
|
2019-08-16 20:35:11 +00:00
|
|
|
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()
|
|
|
|
|
2019-08-22 18:36:07 +00:00
|
|
|
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)
|
|
|
|
pprint(announcedJson)
|
|
|
|
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)
|
|
|
|
pprint(announcedJson)
|
|
|
|
return None
|
|
|
|
if announcedJson['type']!='Note':
|
|
|
|
rejectAnnounce(announceFilename)
|
|
|
|
pprint(announcedJson)
|
|
|
|
return None
|
|
|
|
|
|
|
|
# wrap in create to be consistent with other posts
|
|
|
|
announcedJson= \
|
|
|
|
outboxMessageCreateWrap(httpPrefix, \
|
|
|
|
actorNickname,actorDomain,actorPort, \
|
|
|
|
announcedJson)
|
|
|
|
if announcedJson['type']!='Create':
|
|
|
|
rejectAnnounce(announceFilename)
|
|
|
|
pprint(announcedJson)
|
|
|
|
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
|