epicyon/posts.py

3955 lines
150 KiB
Python
Raw Normal View History

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