epicyon/daemon.py

4601 lines
224 KiB
Python

__filename__ = "daemon.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.0.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net"
__status__ = "Production"
from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
#import socketserver
import commentjson
import json
import time
import base64
import locale
# used for mime decoding of message POST
import email.parser
# for saving images
from binascii import a2b_base64
from hashlib import sha256
from pprint import pprint
from session import createSession
from webfinger import webfingerMeta
from webfinger import webfingerLookup
from webfinger import webfingerHandle
from person import activateAccount
from person import deactivateAccount
from person import registerAccount
from person import personLookup
from person import personBoxJson
from person import createSharedInbox
from person import isSuspended
from person import suspendAccount
from person import unsuspendAccount
from person import removeAccount
from person import canRemovePost
from person import personSnooze
from person import personUnsnooze
from posts import outboxMessageCreateWrap
from posts import savePostToBox
from posts import sendToFollowersThread
from posts import postIsAddressedToPublic
from posts import sendToNamedAddresses
from posts import createPublicPost
from posts import createReportPost
from posts import createUnlistedPost
from posts import createFollowersOnlyPost
from posts import createDirectMessagePost
from posts import populateRepliesJson
from posts import addToField
from posts import expireCache
from inbox import inboxPermittedMessage
from inbox import inboxMessageHasParams
from inbox import runInboxQueue
from inbox import runInboxQueueWatchdog
from inbox import savePostToInboxQueue
from inbox import populateReplies
from inbox import getPersonPubKey
from follow import getFollowingFeed
from follow import outboxUndoFollow
from follow import sendFollowRequest
from auth import authorize
from auth import createPassword
from auth import createBasicAuthHeader
from auth import authorizeBasic
from threads import threadWithTrace
from threads import removeDormantThreads
from media import getMediaPath
from media import createMediaDirs
from delete import outboxDelete
from like import outboxLike
from like import outboxUndoLike
from blocking import outboxBlock
from blocking import outboxUndoBlock
from blocking import addBlock
from blocking import removeBlock
from blocking import addGlobalBlock
from blocking import removeGlobalBlock
from blocking import isBlockedHashtag
from blocking import isBlockedDomain
from config import setConfigParam
from config import getConfigParam
from roles import outboxDelegate
from roles import setRole
from roles import clearModeratorStatus
from skills import outboxSkills
from availability import outboxAvailability
from webinterface import htmlDeletePost
from webinterface import htmlAbout
from webinterface import htmlRemoveSharedItem
from webinterface import htmlInboxDMs
from webinterface import htmlInboxReplies
from webinterface import htmlInboxMedia
from webinterface import htmlUnblockConfirm
from webinterface import htmlPersonOptions
from webinterface import htmlIndividualPost
from webinterface import htmlProfile
from webinterface import htmlInbox
from webinterface import htmlShares
from webinterface import htmlOutbox
from webinterface import htmlModeration
from webinterface import htmlPostReplies
from webinterface import htmlLogin
from webinterface import htmlSuspended
from webinterface import htmlGetLoginCredentials
from webinterface import htmlNewPost
from webinterface import htmlFollowConfirm
from webinterface import htmlCalendar
from webinterface import htmlSearch
from webinterface import htmlSearchEmoji
from webinterface import htmlSearchEmojiTextEntry
from webinterface import htmlUnfollowConfirm
from webinterface import htmlProfileAfterSearch
from webinterface import htmlEditProfile
from webinterface import htmlTermsOfService
from webinterface import htmlSkillsSearch
from webinterface import htmlHashtagSearch
from webinterface import htmlModerationInfo
from webinterface import htmlSearchSharedItems
from webinterface import htmlHashtagBlocked
from shares import getSharesFeedForPerson
from shares import outboxShareUpload
from shares import outboxUndoShareUpload
from shares import addShare
from shares import removeShare
from shares import expireShares
from utils import getNicknameFromActor
from utils import getDomainFromActor
from utils import getStatusNumber
from utils import urlPermitted
from utils import loadJson
from utils import saveJson
from manualapprove import manualDenyFollowRequest
from manualapprove import manualApproveFollowRequest
from announce import createAnnounce
from announce import outboxAnnounce
from content import addHtmlTags
from media import removeMetaData
from cache import storePersonInCache
from httpsig import verifyPostHeaders
import os
import sys
# maximum number of posts to list in outbox feed
maxPostsInFeed=12
# reduced posts for media feed because it can take a while
maxPostsInMediaFeed=6
# number of follows/followers per page
followsPerPage=12
# number of item shares per page
sharesPerPage=12
def readFollowList(filename: str) -> None:
"""Returns a list of ActivityPub addresses to follow
"""
followlist=[]
if not os.path.isfile(filename):
return followlist
followUsers = open(filename, "r")
for u in followUsers:
if u not in followlist:
nickname,domain = parseHandle(u)
if nickname:
followlist.append(nickname+'@'+domain)
followUsers.close()
return followlist
class PubServer(BaseHTTPRequestHandler):
protocol_version = 'HTTP/1.1'
def _requestHTTP(self) -> bool:
"""Should a http response be given?
"""
if not self.headers.get('Accept'):
return False
if 'image/' in self.headers['Accept']:
return False
if self.headers['Accept'].startswith('*'):
return False
if 'json' in self.headers['Accept']:
return False
return True
def _fetchAuthenticated(self) -> bool:
"""http authentication of GET requests for json
"""
if not self.server.authenticatedFetch:
return True
# check that the headers are signed
if not self.headers.get('signature'):
if self.server.debug:
print('WARN: authenticated fetch, GET has no signature in headers')
return False
# get the keyId
keyId=None
signatureParams=self.headers['signature'].split(',')
for signatureItem in signatureParams:
if signatureItem.startswith('keyId='):
if '"' in signatureItem:
keyId=signatureItem.split('"')[1]
break
if not keyId:
if self.server.debug:
print('WARN: authenticated fetch, failed to obtain keyId from signature')
return False
# is the keyId (actor) valid?
if not urlPermitted(keyId,self.server.federationList,"inbox:read"):
if self.server.debug:
print('Authorized fetch failed: '+keyId+' is not permitted')
return False
# make sure we have a session
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session during authenticated fetch')
self.server.session= \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
# obtain the public key
pubKey= \
getPersonPubKey(self.server.baseDir,self.server.session,keyId, \
self.server.personCache,self.server.debug, \
__version__,self.server.httpPrefix, \
self.server.domain)
if not pubKey:
if self.server.debug:
print('DEBUG: Authenticated fetch failed to obtain public key for '+ \
keyId)
return False
# it is assumed that there will be no message body on authenticated fetches
# and also consequently no digest
GETrequestBody=''
GETrequestDigest=None
# verify the GET request without any digest
if verifyPostHeaders(self.server.httpPrefix, \
pubKey,self.headers, \
self.path,True, \
GETrequestDigest,GETrequestBody):
return True
return False
def _login_headers(self,fileFormat: str,length: int) -> None:
self.send_response(200)
self.send_header('Content-type', fileFormat)
self.send_header('Content-Length', str(length))
self.send_header('Host', self.server.domainFull)
self.send_header('WWW-Authenticate', \
'title="Login to Epicyon", Basic realm="epicyon"')
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
def _logout_headers(self,fileFormat: str,length: int) -> None:
self.send_response(200)
self.send_header('Content-type', fileFormat)
self.send_header('Content-Length', str(length))
self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
self.send_header('Host', self.server.domainFull)
self.send_header('WWW-Authenticate', \
'title="Login to Epicyon", Basic realm="epicyon"')
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
def _set_headers(self,fileFormat: str,length: int,cookie: str) -> None:
self.send_response(200)
self.send_header('Content-type', fileFormat)
self.send_header('Content-Length', str(length))
if cookie:
self.send_header('Cookie', cookie)
self.send_header('Host', self.server.domainFull)
self.send_header('InstanceID', self.server.instanceId)
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
def _redirect_headers(self,redirect: str,cookie: str) -> None:
self.send_response(303)
#self.send_header('Content-type', 'text/html')
if cookie:
self.send_header('Cookie', cookie)
self.send_header('Location', redirect)
self.send_header('Host', self.server.domainFull)
self.send_header('InstanceID', self.server.instanceId)
self.send_header('Content-Length', '0')
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
def _404(self) -> None:
msg="<html><head></head><body><h1>404 Not Found</h1></body></html>".encode('utf-8')
self.send_response(404)
self.send_header('Content-Type', 'text/html; charset=utf-8')
self.send_header('Content-Length', str(len(msg)))
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
try:
self.wfile.write(msg)
except Exception as e:
print('Error when showing 404')
print(e)
def _write(self,msg) -> None:
tries=0
while tries<5:
try:
self.wfile.write(msg)
break
except Exception as e:
print(e)
time.sleep(1)
tries+=1
def _robotsTxt(self) -> bool:
if not self.path.lower().startswith('/robot'):
return False
msg='User-agent: *\nDisallow: /'
msg=msg.encode('utf-8')
self._set_headers('text/plain; charset=utf-8',len(msg),None)
self._write(msg)
return True
def _webfinger(self) -> bool:
if not self.path.startswith('/.well-known'):
return False
if self.server.debug:
print('DEBUG: WEBFINGER well-known')
if self.server.debug:
print('DEBUG: WEBFINGER host-meta')
if self.path.startswith('/.well-known/host-meta'):
wfResult=webfingerMeta(self.server.httpPrefix,self.server.domainFull)
if wfResult:
msg=wfResult.encode('utf-8')
self._set_headers('application/xrd+xml',len(msg),None)
self._write(msg)
return
if self.server.debug:
print('DEBUG: WEBFINGER lookup '+self.path+' '+str(self.server.baseDir))
wfResult=webfingerLookup(self.path,self.server.baseDir,self.server.port,self.server.debug)
if wfResult:
msg=json.dumps(wfResult).encode('utf-8')
self._set_headers('application/jrd+json',len(msg),None)
self._write(msg)
else:
if self.server.debug:
print('DEBUG: WEBFINGER lookup 404 '+self.path)
self._404()
return True
def _permittedDir(self,path: str) -> bool:
"""These are special paths which should not be accessible
directly via GET or POST
"""
if path.startswith('/wfendpoints') or \
path.startswith('/keys') or \
path.startswith('/accounts'):
return False
return True
def _postToOutbox(self,messageJson: {},version: str) -> bool:
"""post is received by the outbox
Client to server message post
https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
"""
if not messageJson.get('type'):
if self.server.debug:
print('DEBUG: POST to outbox has no "type" parameter')
return False
if not messageJson.get('object') and messageJson.get('content'):
if messageJson['type']!='Create':
# https://www.w3.org/TR/activitypub/#object-without-create
if self.server.debug:
print('DEBUG: POST to outbox - adding Create wrapper')
messageJson= \
outboxMessageCreateWrap(self.server.httpPrefix, \
self.postToNickname, \
self.server.domain, \
self.server.port, \
messageJson)
if messageJson['type']=='Create':
if not (messageJson.get('id') and \
messageJson.get('type') and \
messageJson.get('actor') and \
messageJson.get('object') and \
messageJson.get('to')):
if self.server.debug:
pprint(messageJson)
print('DEBUG: POST to outbox - Create does not have the required parameters')
return False
testDomain,testPort=getDomainFromActor(messageJson['actor'])
if testPort:
if testPort!=80 and testPort!=443:
testDomain=testDomain+':'+str(testPort)
if isBlockedDomain(self.server.baseDir,testDomain):
if self.server.debug:
print('DEBUG: domain is blocked: '+messageJson['actor'])
return False
# https://www.w3.org/TR/activitypub/#create-activity-outbox
messageJson['object']['attributedTo']=messageJson['actor']
if messageJson['object'].get('attachment'):
attachmentIndex=0
if messageJson['object']['attachment'][attachmentIndex].get('mediaType'):
fileExtension='png'
mediaTypeStr= \
messageJson['object']['attachment'][attachmentIndex]['mediaType']
if mediaTypeStr.endswith('jpeg'):
fileExtension='jpg'
elif mediaTypeStr.endswith('gif'):
fileExtension='gif'
elif mediaTypeStr.endswith('audio/mpeg'):
fileExtension='mp3'
elif mediaTypeStr.endswith('ogg'):
fileExtension='ogg'
elif mediaTypeStr.endswith('mp4'):
fileExtension='mp4'
elif mediaTypeStr.endswith('webm'):
fileExtension='webm'
elif mediaTypeStr.endswith('ogv'):
fileExtension='ogv'
mediaDir= \
self.server.baseDir+'/accounts/'+ \
self.postToNickname+'@'+self.server.domain
uploadMediaFilename=mediaDir+'/upload.'+fileExtension
if not os.path.isfile(uploadMediaFilename):
del messageJson['object']['attachment']
else:
# generate a path for the uploaded image
mPath=getMediaPath()
mediaPath=mPath+'/'+createPassword(32)+'.'+fileExtension
createMediaDirs(self.server.baseDir,mPath)
mediaFilename=self.server.baseDir+'/'+mediaPath
# move the uploaded image to its new path
os.rename(uploadMediaFilename,mediaFilename)
# change the url of the attachment
messageJson['object']['attachment'][attachmentIndex]['url']= \
self.server.httpPrefix+'://'+self.server.domainFull+ \
'/'+mediaPath
permittedOutboxTypes=[
'Create','Announce','Like','Follow','Undo', \
'Update','Add','Remove','Block','Delete', \
'Delegate','Skill'
]
if messageJson['type'] not in permittedOutboxTypes:
if self.server.debug:
print('DEBUG: POST to outbox - '+messageJson['type']+ \
' is not a permitted activity type')
return False
if messageJson.get('id'):
postId=messageJson['id'].replace('/activity','').replace('/undo','')
if self.server.debug:
print('DEBUG: id attribute exists within POST to outbox')
else:
if self.server.debug:
print('DEBUG: No id attribute within POST to outbox')
postId=None
if self.server.debug:
pprint(messageJson)
print('DEBUG: savePostToBox')
savePostToBox(self.server.baseDir, \
self.server.httpPrefix, \
postId, \
self.postToNickname, \
self.server.domainFull,messageJson,'outbox')
if outboxAnnounce(self.server.baseDir,messageJson,self.server.debug):
if self.server.debug:
print('DEBUG: Updated announcements (shares) collection for the post associated with the Announce activity')
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session for c2s')
self.server.session= \
createSession(self.server.domain,self.server.port,self.server.useTor)
if self.server.debug:
print('DEBUG: sending c2s post to followers')
# remove inactive threads
inactiveFollowerThreads=[]
for th in self.server.followersThreads:
if not th.is_alive():
inactiveFollowerThreads.append(th)
for th in inactiveFollowerThreads:
self.server.followersThreads.remove(th)
# create a thread to send the post to followers
followersThread= \
sendToFollowersThread(self.server.session, \
self.server.baseDir, \
self.postToNickname, \
self.server.domain, \
self.server.port, \
self.server.httpPrefix, \
self.server.federationList, \
self.server.sendThreads, \
self.server.postLog, \
self.server.cachedWebfingers, \
self.server.personCache, \
messageJson,self.server.debug, \
self.server.projectVersion)
self.server.followersThreads.append(followersThread)
# retain up to 10 threads
if len(self.server.followersThreads)>10:
for i in range(2):
# kill the thread if it is still alive
if self.server.followersThreads[0].is_alive():
self.server.followersThreads[0].kill()
# remove it from the list
self.server.followersThreads.pop(0)
if self.server.debug:
print('DEBUG: handle any unfollow requests')
outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle delegation requests')
outboxDelegate(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle skills changes requests')
outboxSkills(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle availability changes requests')
outboxAvailability(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle any like requests')
outboxLike(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain,self.server.port, \
messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle any undo like requests')
outboxUndoLike(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain,self.server.port, \
messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle delete requests')
outboxDelete(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain, \
messageJson,self.server.debug, \
self.server.allowDeletion)
if self.server.debug:
print('DEBUG: handle block requests')
outboxBlock(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain, \
self.server.port,
messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle undo block requests')
outboxUndoBlock(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain, \
self.server.port,
messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle share uploads')
outboxShareUpload(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain, \
self.server.port,
messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: handle undo share uploads')
outboxUndoShareUpload(self.server.baseDir,self.server.httpPrefix, \
self.postToNickname,self.server.domain, \
self.server.port,
messageJson,self.server.debug)
if self.server.debug:
print('DEBUG: sending c2s post to named addresses')
print('c2s sender: '+self.postToNickname+'@'+ \
self.server.domain+':'+str(self.server.port))
sendToNamedAddresses(self.server.session,self.server.baseDir, \
self.postToNickname,self.server.domain, \
self.server.port, \
self.server.httpPrefix, \
self.server.federationList, \
self.server.sendThreads, \
self.server.postLog, \
self.server.cachedWebfingers, \
self.server.personCache, \
messageJson,self.server.debug, \
self.server.projectVersion)
return True
def _postToOutboxThread(self,messageJson: {}) -> bool:
"""Creates a thread to send a post
"""
accountOutboxThreadName=self.postToNickname
if not accountOutboxThreadName:
accountOutboxThreadName='*'
if self.server.outboxThread.get(accountOutboxThreadName):
print('Waiting for previous outbox thread to end')
waitCtr=0
while self.server.outboxThread[accountOutboxThreadName].isAlive() and waitCtr<8:
time.sleep(1)
waitCtr+=1
if waitCtr>=8:
self.server.outboxThread[accountOutboxThreadName].kill()
print('Creating outbox thread')
self.server.outboxThread[accountOutboxThreadName]= \
threadWithTrace(target=self._postToOutbox, \
args=(messageJson.copy(),__version__),daemon=True)
print('Starting outbox thread')
self.server.outboxThread[accountOutboxThreadName].start()
return True
def _inboxQueueCleardown(self) -> None:
""" Check if the queue is full and remove oldest items if it is
"""
if len(self.server.inboxQueue)>=self.server.maxQueueLength:
print('Inbox queue is full. Removing oldest items.')
while len(self.server.inboxQueue) >= self.server.maxQueueLength-4:
queueFilename=self.server.inboxQueue[0]
if os.path.isfile(queueFilename):
os.remove(queueFilename)
self.server.inboxQueue.pop(0)
def _updateInboxQueue(self,nickname: str,messageJson: {}, \
messageBytes: str) -> int:
"""Update the inbox queue
"""
self._inboxQueueCleardown()
# Convert the headers needed for signature verification to dict
headersDict={}
headersDict['host']=self.headers['host']
headersDict['signature']=self.headers['signature']
if self.headers.get('Date'):
headersDict['Date']=self.headers['Date']
if self.headers.get('digest'):
headersDict['digest']=self.headers['digest']
if self.headers.get('Content-type'):
headersDict['Content-type']=self.headers['Content-type']
# For follow activities add a 'to' field, which is a copy
# of the object field
messageJson,toFieldExists= \
addToField('Follow',messageJson,self.server.debug)
# For like activities add a 'to' field, which is a copy of
# the actor within the object field
messageJson,toFieldExists= \
addToField('Like',messageJson,self.server.debug)
#pprint(messageJson)
# save the json for later queue processing
queueFilename = \
savePostToInboxQueue(self.server.baseDir,
self.server.httpPrefix,
nickname,
self.server.domainFull,
messageJson,
messageBytes.decode('utf-8'),
headersDict,
self.path,
self.server.debug)
if queueFilename:
# add json to the queue
if queueFilename not in self.server.inboxQueue:
self.server.inboxQueue.append(queueFilename)
self.send_response(201)
self.end_headers()
self.server.POSTbusy=False
return 0
return 2
def _isAuthorized(self) -> bool:
# token based authenticated used by the web interface
if self.headers.get('Cookie'):
if '=' in self.headers['Cookie']:
tokenStr=self.headers['Cookie'].split('=',1)[1]
if self.server.tokensLookup.get(tokenStr):
nickname=self.server.tokensLookup[tokenStr]
# default to the inbox of the person
if self.path=='/':
self.path='/users/'+nickname+'/inbox'
# check that the path contains the same nickname as the cookie
# otherwise it would be possible to be authorized to use
# an account you don't own
if '/'+nickname+'/' in self.path:
return True
if self.path.endswith('/'+nickname):
return True
return False
# basic auth
if self.headers.get('Authorization'):
if authorize(self.server.baseDir,self.path, \
self.headers['Authorization'], \
self.server.debug):
return True
return False
def _clearLoginDetails(self,nickname: str):
"""Clears login details for the given account
"""
# remove any token
if self.server.tokens.get(nickname):
del self.server.tokensLookup[self.server.tokens[nickname]]
del self.server.tokens[nickname]
self.send_response(303)
self.send_header('Content-Length', '0')
self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
self.send_header('Location', '/login')
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
def do_GET(self):
if self.path=='/logout':
msg=htmlLogin(self.server.translate, \
self.server.baseDir,False).encode('utf-8')
self._logout_headers('text/html',len(msg))
self._write(msg)
return
# redirect music to #nowplaying list
if self.path=='/music' or self.path=='/nowplaying':
self.path='/tags/nowplaying'
if self.server.debug:
print('DEBUG: GET from '+self.server.baseDir+ \
' path: '+self.path+' busy: '+ \
str(self.server.GETbusy))
if self.server.debug:
print(str(self.headers))
cookie=None
if self.headers.get('Cookie'):
cookie=self.headers['Cookie']
# check authorization
authorized = self._isAuthorized()
if authorized:
if self.server.debug:
print('GET Authorization granted')
else:
if self.server.debug:
print('GET Not authorized')
if not self.server.session:
self.server.session= \
createSession(self.server.domain, \
self.server.port,self.server.useTor)
# is this a html request?
htmlGET=False
if self.headers.get('Accept'):
if self._requestHTTP():
htmlGET=True
# replace https://domain/@nick with https://domain/users/nick
if self.path.startswith('/@'):
self.path=self.path.replace('/@','/users/')
# treat shared inbox paths consistently
if self.path=='/sharedInbox' or \
self.path=='/users/inbox' or \
self.path=='/actor/inbox' or \
self.path=='/users/'+self.server.domain:
self.path='/inbox'
# show the person options screen with view/follow/block/report
if htmlGET and '/users/' in self.path:
if '?options=' in self.path:
optionsStr=self.path.split('?options=')[1]
originPathStr=self.path.split('?options=')[0]
if ';' in optionsStr:
pageNumber=1
optionsList=optionsStr.split(';')
optionsActor=optionsList[0]
optionsPageNumber=optionsList[1]
optionsProfileUrl=optionsList[2]
if optionsPageNumber.isdigit():
pageNumber=int(optionsPageNumber)
optionsLink=None
if len(optionsList)>3:
optionsLink=optionsList[3]
msg=htmlPersonOptions(self.server.translate, \
self.server.baseDir, \
self.server.domain, \
originPathStr, \
optionsActor, \
optionsProfileUrl, \
optionsLink, \
pageNumber).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
self._redirect_headers(originPathStr,cookie)
self.server.GETbusy=False
return
# remove a shared item
if htmlGET and '?rmshare=' in self.path:
shareName=self.path.split('?rmshare=')[1]
shareName=shareName.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').strip()
actor= \
self.server.httpPrefix+'://'+self.server.domainFull+ \
self.path.split('?rmshare=')[0]
msg=htmlRemoveSharedItem(self.server.translate, \
self.server.baseDir, \
actor,shareName).encode()
if not msg:
self._redirect_headers(actor+'/inbox',cookie)
self.server.GETbusy=False
return
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
if self.path.startswith('/terms'):
msg=htmlTermsOfService(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domainFull).encode()
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.GETbusy=False
return
if self.path.startswith('/about'):
msg=htmlAbout(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domainFull).encode()
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.GETbusy=False
return
# send robots.txt if asked
if self._robotsTxt():
self.server.GETbusy=False
return
# if not authorized then show the login screen
if htmlGET and self.path!='/login' and self.path!='/':
if '/media/' not in self.path and \
'/sharefiles/' not in self.path and \
'/statuses/' not in self.path and \
'/emoji/' not in self.path and \
'/tags/' not in self.path and \
'/avatars/' not in self.path and \
'/icons/' not in self.path:
divertToLoginScreen=True
if self.path.startswith('/users/'):
nickStr=self.path.split('/users/')[1]
if '/' not in nickStr and '?' not in nickStr:
divertToLoginScreen=False
else:
if self.path.endswith('/following') or \
self.path.endswith('/followers') or \
self.path.endswith('/skills') or \
self.path.endswith('/roles') or \
self.path.endswith('/shares'):
divertToLoginScreen=False
if divertToLoginScreen and not authorized:
if self.server.debug:
print('DEBUG: divertToLoginScreen='+str(divertToLoginScreen))
print('DEBUG: authorized='+str(authorized))
print('DEBUG: path='+self.path)
self.send_response(303)
self.send_header('Location', '/login')
self.send_header('Content-Length', '0')
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
self.server.GETbusy=False
return
# get css
# Note that this comes before the busy flag to avoid conflicts
if self.path.endswith('.css'):
if os.path.isfile('epicyon-profile.css'):
tries=0
while tries<5:
try:
with open('epicyon-profile.css', 'r') as cssfile:
css = cssfile.read()
break
except Exception as e:
print(e)
time.sleep(1)
tries+=1
msg=css.encode('utf-8')
self._set_headers('text/css',len(msg),cookie)
self._write(msg)
return
# image on login screen
if self.path=='/login.png':
mediaFilename= \
self.server.baseDir+'/accounts/login.png'
if os.path.isfile(mediaFilename):
tries=0
mediaBinary=None
while tries<5:
try:
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
break
except Exception as e:
print(e)
time.sleep(1)
tries+=1
if mediaBinary:
self._set_headers('image/png',len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# login screen background image
if self.path=='/login-background.png':
mediaFilename= \
self.server.baseDir+'/accounts/login-background.png'
if os.path.isfile(mediaFilename):
tries=0
mediaBinary=None
while tries<5:
try:
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
break
except Exception as e:
print(e)
time.sleep(1)
tries+=1
if mediaBinary:
self._set_headers('image/png',len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# follow screen background image
if self.path=='/follow-background.png':
mediaFilename= \
self.server.baseDir+'/accounts/follow-background.png'
if os.path.isfile(mediaFilename):
tries=0
mediaBinary=None
while tries<5:
try:
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
break
except Exception as e:
print(e)
time.sleep(1)
tries+=1
if mediaBinary:
self._set_headers('image/png',len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# emoji images
if '/emoji/' in self.path:
if self.path.endswith('.png') or \
self.path.endswith('.jpg') or \
self.path.endswith('.gif'):
emojiStr=self.path.split('/emoji/')[1]
emojiFilename= \
self.server.baseDir+'/emoji/'+emojiStr
if os.path.isfile(emojiFilename):
mediaImageType='png'
if emojiFilename.endswith('.png'):
mediaImageType='png'
elif emojiFilename.endswith('.jpg'):
mediaImageType='jpeg'
else:
mediaImageType='gif'
with open(emojiFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers('image/'+mediaImageType,len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# show media
# Note that this comes before the busy flag to avoid conflicts
if '/media/' in self.path:
if self.path.endswith('.png') or \
self.path.endswith('.jpg') or \
self.path.endswith('.gif') or \
self.path.endswith('.mp4') or \
self.path.endswith('.ogv') or \
self.path.endswith('.mp3') or \
self.path.endswith('.ogg'):
mediaStr=self.path.split('/media/')[1]
mediaFilename= \
self.server.baseDir+'/media/'+mediaStr
if os.path.isfile(mediaFilename):
mediaFileType='image/png'
if mediaFilename.endswith('.png'):
mediaFileType='image/png'
elif mediaFilename.endswith('.jpg'):
mediaFileType='image/jpeg'
elif mediaFilename.endswith('.gif'):
mediaFileType='image/gif'
elif mediaFilename.endswith('.mp4'):
mediaFileType='video/mp4'
elif mediaFilename.endswith('.ogv'):
mediaFileType='video/ogv'
elif mediaFilename.endswith('.mp3'):
mediaFileType='audio/mpeg'
elif mediaFilename.endswith('.ogg'):
mediaFileType='audio/ogg'
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers(mediaFileType,len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# show shared item images
# Note that this comes before the busy flag to avoid conflicts
if '/sharefiles/' in self.path:
if self.path.endswith('.png') or \
self.path.endswith('.jpg') or \
self.path.endswith('.gif'):
mediaStr=self.path.split('/sharefiles/')[1]
mediaFilename= \
self.server.baseDir+'/sharefiles/'+mediaStr
if os.path.isfile(mediaFilename):
mediaFileType='png'
if mediaFilename.endswith('.png'):
mediaFileType='png'
elif mediaFilename.endswith('.jpg'):
mediaFileType='jpeg'
else:
mediaFileType='gif'
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers('image/'+mediaFileType,len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# icon images
# Note that this comes before the busy flag to avoid conflicts
if self.path.startswith('/icons/'):
if self.path.endswith('.png'):
mediaStr=self.path.split('/icons/')[1]
mediaFilename= \
self.server.baseDir+'/img/icons/'+mediaStr
if os.path.isfile(mediaFilename):
if mediaFilename.endswith('.png'):
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers('image/png',len(mediaBinary),cookie)
self._write(mediaBinary)
return
self._404()
return
# cached avatar images
# Note that this comes before the busy flag to avoid conflicts
if self.path.startswith('/avatars/'):
mediaFilename= \
self.server.baseDir+'/cache/'+self.path
if os.path.isfile(mediaFilename):
with open(mediaFilename, 'rb') as avFile:
mediaBinary = avFile.read()
if mediaFilename.endswith('.png'):
self._set_headers('image/png',len(mediaBinary),cookie)
elif mediaFilename.endswith('.jpg'):
self._set_headers('image/jpeg',len(mediaBinary),cookie)
elif mediaFilename.endswith('.gif'):
self._set_headers('image/gif',len(mediaBinary),cookie)
else:
# default to jpeg
self._set_headers('image/jpeg',len(mediaBinary),cookie)
#self._404()
return
self._write(mediaBinary)
return
self._404()
return
# show avatar or background image
# Note that this comes before the busy flag to avoid conflicts
if '/users/' in self.path:
if self.path.endswith('.png') or \
self.path.endswith('.jpg') or \
self.path.endswith('.gif'):
avatarStr=self.path.split('/users/')[1]
if '/' in avatarStr:
avatarNickname=avatarStr.split('/')[0]
avatarFile=avatarStr.split('/')[1]
avatarFilename= \
self.server.baseDir+'/accounts/'+ \
avatarNickname+'@'+ \
self.server.domain+'/'+avatarFile
if os.path.isfile(avatarFilename):
mediaImageType='png'
if avatarFile.endswith('.png'):
mediaImageType='png'
elif avatarFile.endswith('.jpg'):
mediaImageType='jpeg'
else:
mediaImageType='gif'
with open(avatarFilename, 'rb') as avFile:
mediaBinary = avFile.read()
self._set_headers('image/'+mediaImageType, \
len(mediaBinary),cookie)
self._write(mediaBinary)
return
# This busy state helps to avoid flooding
# Resources which are expected to be called from a web page
# should be above this
if self.server.GETbusy:
currTimeGET=int(time.time())
if currTimeGET-self.server.lastGET==0:
if self.server.debug:
print('DEBUG: GET Busy')
self.send_response(429)
self.end_headers()
return
self.server.lastGET=currTimeGET
self.server.GETbusy=True
if not self._permittedDir(self.path):
if self.server.debug:
print('DEBUG: GET Not permitted')
self._404()
self.server.GETbusy=False
return
# get webfinger endpoint for a person
if self._webfinger():
self.server.GETbusy=False
return
if self.path.startswith('/login') or \
(self.path=='/' and not authorized):
# request basic auth
msg=htmlLogin(self.server.translate, \
self.server.baseDir).encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.GETbusy=False
return
# hashtag search
if self.path.startswith('/tags/'):
pageNumber=1
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
hashtag=self.path.split('/tags/')[1]
if '?page=' in hashtag:
hashtag=hashtag.split('?page=')[0]
if isBlockedHashtag(self.server.baseDir,hashtag):
msg=htmlHashtagBlocked(self.server.baseDir).encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.GETbusy=False
return
hashtagStr= \
htmlHashtagSearch(self.server.translate, \
self.server.baseDir,hashtag,pageNumber, \
maxPostsInFeed,self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.httpPrefix, \
self.server.projectVersion)
if hashtagStr:
msg=hashtagStr.encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
originPathStr=self.path.split('/tags/')[0]
self._redirect_headers(originPathStr+'/search',cookie)
self.server.GETbusy=False
return
# search for a fediverse address, shared item or emoji
# from the web interface by selecting search icon
if htmlGET and '/users/' in self.path:
if self.path.endswith('/search'):
# show the search screen
msg=htmlSearch(self.server.translate, \
self.server.baseDir,self.path).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
# Show the calendar for a user
if htmlGET and '/users/' in self.path:
if '/calendar' in self.path:
# show the calendar screen
msg=htmlCalendar(self.server.translate, \
self.server.baseDir,self.path, \
self.server.httpPrefix, \
self.server.domainFull).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
# search for emoji by name
if htmlGET and '/users/' in self.path:
if self.path.endswith('/searchemoji'):
# show the search screen
msg=htmlSearchEmojiTextEntry(self.server.translate, \
self.server.baseDir, \
self.path).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
# announce/repeat from the web interface
if htmlGET and '?repeat=' in self.path:
pageNumber=1
repeatUrl=self.path.split('?repeat=')[1]
if '?' in repeatUrl:
repeatUrl=repeatUrl.split('?')[0]
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr=pageNumberStr.split('?')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
timelineStr='inbox'
if '?tl=' in self.path:
timelineStr=self.path.split('?tl=')[1]
if '?' in timelineStr:
timelineStr=timelineStr.split('?')[0]
actor=self.path.split('?repeat=')[0]
self.postToNickname=getNicknameFromActor(actor)
if not self.postToNickname:
print('WARN: unable to find nickname in '+actor)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+ \
'?page='+str(pageNumber),cookie)
return
if not self.server.session:
self.server.session= \
createSession(self.server.domain, \
self.server.port,self.server.useTor)
announceJson= \
createAnnounce(self.server.session, \
self.server.baseDir, \
self.server.federationList, \
self.postToNickname, \
self.server.domain,self.server.port, \
'https://www.w3.org/ns/activitystreams#Public', \
None,self.server.httpPrefix, \
repeatUrl,False,False, \
self.server.sendThreads, \
self.server.postLog, \
self.server.personCache, \
self.server.cachedWebfingers, \
self.server.debug, \
self.server.projectVersion)
if announceJson:
self._postToOutboxThread(announceJson)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+'?page='+ \
str(pageNumber),cookie)
return
# undo an announce/repeat from the web interface
if htmlGET and '?unrepeat=' in self.path:
pageNumber=1
repeatUrl=self.path.split('?unrepeat=')[1]
if '?' in repeatUrl:
repeatUrl=repeatUrl.split('?')[0]
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr=pageNumberStr.split('?')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
timelineStr='inbox'
if '?tl=' in self.path:
timelineStr=self.path.split('?tl=')[1]
if '?' in timelineStr:
timelineStr=timelineStr.split('?')[0]
actor=self.path.split('?unrepeat=')[0]
self.postToNickname=getNicknameFromActor(actor)
if not self.postToNickname:
print('WARN: unable to find nickname in '+actor)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+'?page='+ \
str(pageNumber),cookie)
return
if not self.server.session:
self.server.session= \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
undoAnnounceActor= \
self.server.httpPrefix+'://'+self.server.domainFull+ \
'/users/'+self.postToNickname
newUndoAnnounce = {
"@context": "https://www.w3.org/ns/activitystreams",
'actor': undoAnnounceActor,
'type': 'Undo',
'cc': [undoAnnounceActor+'/followers'],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'object': {
'actor': undoAnnounceActor,
'cc': [undoAnnounceActor+'/followers'],
'object': repeatUrl,
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'type': 'Announce'
}
}
self._postToOutboxThread(newUndoAnnounce)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+'?page='+ \
str(pageNumber),cookie)
return
# send a follow request approval from the web interface
if authorized and '/followapprove=' in self.path and \
self.path.startswith('/users/'):
originPathStr=self.path.split('/followapprove=')[0]
followerNickname=originPathStr.replace('/users/','')
followingHandle=self.path.split('/followapprove=')[1]
if '@' in followingHandle:
if not self.server.session:
self.server.session= \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
manualApproveFollowRequest(self.server.session, \
self.server.baseDir, \
self.server.httpPrefix, \
followerNickname, \
self.server.domain, \
self.server.port, \
followingHandle, \
self.server.federationList, \
self.server.sendThreads, \
self.server.postLog, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.acceptedCaps, \
self.server.debug, \
self.server.projectVersion)
self._redirect_headers(originPathStr,cookie)
self.server.GETbusy=False
return
# deny a follow request from the web interface
if authorized and '/followdeny=' in self.path and \
self.path.startswith('/users/'):
originPathStr=self.path.split('/followdeny=')[0]
followerNickname=originPathStr.replace('/users/','')
followingHandle=self.path.split('/followdeny=')[1]
if '@' in followingHandle:
manualDenyFollowRequest(self.server.session, \
self.server.baseDir, \
self.server.httpPrefix, \
followerNickname, \
self.server.domain, \
self.server.port, \
followingHandle, \
self.server.federationList, \
self.server.sendThreads, \
self.server.postLog, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.debug, \
self.server.projectVersion)
self._redirect_headers(originPathStr,cookie)
self.server.GETbusy=False
return
# like from the web interface icon
if htmlGET and '?like=' in self.path and '/statuses/' in self.path:
pageNumber=1
likeUrl=self.path.split('?like=')[1]
if '?' in likeUrl:
likeUrl=likeUrl.split('?')[0]
actor=self.path.split('?like=')[0]
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr=pageNumberStr.split('?')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
timelineStr='inbox'
if '?tl=' in self.path:
timelineStr=self.path.split('?tl=')[1]
if '?' in timelineStr:
timelineStr=timelineStr.split('?')[0]
self.postToNickname=getNicknameFromActor(actor)
if not self.postToNickname:
print('WARN: unable to find nickname in '+actor)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+ \
'?page='+str(pageNumber),cookie)
return
if not self.server.session:
self.server.session= \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
likeActor= \
self.server.httpPrefix+'://'+ \
self.server.domainFull+'/users/'+self.postToNickname
actorLiked=likeUrl.split('/statuses/')[0]
likeJson= {
"@context": "https://www.w3.org/ns/activitystreams",
'type': 'Like',
'actor': likeActor,
'object': likeUrl
}
self._postToOutboxThread(likeJson)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+ \
'?page='+str(pageNumber),cookie)
return
# undo a like from the web interface icon
if htmlGET and '?unlike=' in self.path and '/statuses/' in self.path:
pageNumber=1
likeUrl=self.path.split('?unlike=')[1]
if '?' in likeUrl:
likeUrl=likeUrl.split('?')[0]
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr=pageNumberStr.split('?')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
timelineStr='inbox'
if '?tl=' in self.path:
timelineStr=self.path.split('?tl=')[1]
if '?' in timelineStr:
timelineStr=timelineStr.split('?')[0]
actor=self.path.split('?unlike=')[0]
self.postToNickname=getNicknameFromActor(actor)
if not self.postToNickname:
print('WARN: unable to find nickname in '+actor)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+ \
'?page='+str(pageNumber),cookie)
return
if not self.server.session:
self.server.session= \
createSession(self.server.domain, \
self.server.port, \
self.server.useTor)
undoActor= \
self.server.httpPrefix+'://'+ \
self.server.domainFull+'/users/'+self.postToNickname
actorLiked=likeUrl.split('/statuses/')[0]
undoLikeJson= {
"@context": "https://www.w3.org/ns/activitystreams",
'type': 'Undo',
'actor': undoActor,
'object': {
'type': 'Like',
'actor': undoActor,
'object': likeUrl
}
}
self._postToOutboxThread(undoLikeJson)
self.server.GETbusy=False
self._redirect_headers(actor+'/'+timelineStr+ \
'?page='+str(pageNumber),cookie)
return
# delete a post from the web interface icon
if htmlGET and '?delete=' in self.path:
pageNumber=1
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr=pageNumberStr.split('?')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
deleteUrl=self.path.split('?delete=')[1]
if '?' in deleteUrl:
deleteUrl=deleteUrl.split('?')[0]
actor= \
self.server.httpPrefix+'://'+ \
self.server.domainFull+self.path.split('?delete=')[0]
if self.server.allowDeletion or \
deleteUrl.startswith(actor):
if self.server.debug:
print('DEBUG: deleteUrl='+deleteUrl)
print('DEBUG: actor='+actor)
if actor not in deleteUrl:
# You can only delete your own posts
self.server.GETbusy=False
self._redirect_headers(actor+'/inbox',cookie)
return
self.postToNickname=getNicknameFromActor(actor)
if not self.postToNickname:
print('WARN: unable to find nickname in '+actor)
self.server.GETbusy=False
self._redirect_headers(actor+'/inbox',cookie)
return
if not self.server.session:
self.server.session= \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
deleteStr= \
htmlDeletePost(self.server.translate,pageNumber, \
self.server.session,self.server.baseDir, \
deleteUrl,self.server.httpPrefix, \
__version__,self.server.cachedWebfingers, \
self.server.personCache)
if deleteStr:
self._set_headers('text/html',len(deleteStr),cookie)
self._write(deleteStr.encode())
self.server.GETbusy=False
return
self.server.GETbusy=False
self._redirect_headers(actor+'/inbox',cookie)
return
# reply from the web interface icon
inReplyToUrl=None
replyWithDM=False
replyToList=[]
replyPageNumber=1
shareDescription=None
if htmlGET:
# public reply
if '?replyto=' in self.path:
inReplyToUrl=self.path.split('?replyto=')[1]
if '?' in inReplyToUrl:
mentionsList=inReplyToUrl.split('?')
for m in mentionsList:
if m.startswith('mention='):
replyHandle=m.replace('mention=','')
if replyHandle not in replyToList:
replyToList.append(replyHandle)
if m.startswith('page='):
replyPageStr=m.replace('page=','')
if replyPageStr.isdigit():
replyPageNumber=int(replyPageStr)
inReplyToUrl=mentionsList[0]
self.path=self.path.split('?replyto=')[0]+'/newpost'
if self.server.debug:
print('DEBUG: replyto path '+self.path)
# reply to followers
if '?replyfollowers=' in self.path:
inReplyToUrl=self.path.split('?replyfollowers=')[1]
if '?' in inReplyToUrl:
mentionsList=inReplyToUrl.split('?')
for m in mentionsList:
if m.startswith('mention='):
replyHandle=m.replace('mention=','')
if m.replace('mention=','') not in replyToList:
replyToList.append(replyHandle)
if m.startswith('page='):
replyPageStr=m.replace('page=','')
if replyPageStr.isdigit():
replyPageNumber=int(replyPageStr)
inReplyToUrl=mentionsList[0]
self.path=self.path.split('?replyfollowers=')[0]+'/newfollowers'
if self.server.debug:
print('DEBUG: replyfollowers path '+self.path)
# replying as a direct message, for moderation posts or the dm timeline
if '?replydm=' in self.path:
inReplyToUrl=self.path.split('?replydm=')[1]
if '?' in inReplyToUrl:
mentionsList=inReplyToUrl.split('?')
for m in mentionsList:
if m.startswith('mention='):
replyHandle=m.replace('mention=','')
if m.replace('mention=','') not in replyToList:
replyToList.append(m.replace('mention=',''))
if m.startswith('page='):
replyPageStr=m.replace('page=','')
if replyPageStr.isdigit():
replyPageNumber=int(replyPageStr)
inReplyToUrl=mentionsList[0]
if inReplyToUrl.startswith('sharedesc:'):
shareDescription= \
inReplyToUrl.replace('sharedesc:','').replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#')
self.path=self.path.split('?replydm=')[0]+'/newdm'
if self.server.debug:
print('DEBUG: replydm path '+self.path)
# edit profile in web interface
if '/users/' in self.path and self.path.endswith('/editprofile'):
msg=htmlEditProfile(self.server.translate, \
self.server.baseDir, \
self.path,self.server.domain, \
self.server.port).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
# Various types of new post in the web interface
if '/users/' in self.path and \
(self.path.endswith('/newpost') or \
self.path.endswith('/newunlisted') or \
self.path.endswith('/newfollowers') or \
self.path.endswith('/newdm') or \
self.path.endswith('/newreport') or \
self.path.endswith('/newshare')):
msg=htmlNewPost(self.server.translate, \
self.server.baseDir, \
self.path,inReplyToUrl, \
replyToList, \
shareDescription, \
replyPageNumber).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
# get an individual post from the path /@nickname/statusnumber
if '/@' in self.path:
namedStatus=self.path.split('/@')[1]
if '/' not in namedStatus:
# show actor
nickname=namedStatus
else:
postSections=namedStatus.split('/')
if len(postSections)==2:
nickname=postSections[0]
statusNumber=postSections[1]
if len(statusNumber)>10 and statusNumber.isdigit():
postFilename= \
self.server.baseDir+'/accounts/'+nickname+'@'+ \
self.server.domain+'/outbox/'+ \
self.server.httpPrefix+':##'+ \
self.server.domainFull+'#users#'+ \
nickname+'#statuses#'+statusNumber+'.json'
if os.path.isfile(postFilename):
postJsonObject=loadJson(postFilename)
loadedPost=False
if postJsonObject:
loadedPost=True
else:
postJsonObject={}
if loadedPost:
# Only authorized viewers get to see likes on posts
# Otherwize marketers could gain more social graph info
if not authorized:
if postJsonObject.get('likes'):
postJsonObject['likes']={'items': []}
if self._requestHTTP():
msg= \
htmlIndividualPost(self.server.translate, \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname,self.server.domain, \
self.server.port, \
authorized,postJsonObject, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(postJsonObject).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
else:
self._404()
self.server.GETbusy=False
return
# get replies to a post /users/nickname/statuses/number/replies
if self.path.endswith('/replies') or '/replies?page=' in self.path:
if '/statuses/' in self.path and '/users/' in self.path:
namedStatus=self.path.split('/users/')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
if len(postSections)>=4:
if postSections[3].startswith('replies'):
nickname=postSections[0]
statusNumber=postSections[2]
if len(statusNumber)>10 and statusNumber.isdigit():
#get the replies file
boxname='outbox'
postDir= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/'+boxname
postRepliesFilename= \
postDir+'/'+ \
self.server.httpPrefix+':##'+ \
self.server.domainFull+'#users#'+ \
nickname+'#statuses#'+statusNumber+'.replies'
if not os.path.isfile(postRepliesFilename):
# There are no replies, so show empty collection
repliesJson = {
'@context': 'https://www.w3.org/ns/activitystreams',
'first': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
'id': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
'last': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
'totalItems': 0,
'type': 'OrderedCollection'}
if self._requestHTTP():
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session')
self.server.session= \
createSession(self.server.domain, \
self.server.port, \
self.server.useTor)
msg=htmlPostReplies(self.server.translate, \
self.server.baseDir, \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
repliesJson, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
print('----------------------------------------------------')
#pprint(repliesJson)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(repliesJson).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
else:
# replies exist. Itterate through the text file containing message ids
repliesJson = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'?page=true',
'orderedItems': [
],
'partOf': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber,
'type': 'OrderedCollectionPage'}
# populate the items list with replies
populateRepliesJson(self.server.baseDir, \
nickname, \
self.server.domain, \
postRepliesFilename, \
authorized, \
repliesJson)
# send the replies json
if self._requestHTTP():
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session')
self.server.session= \
createSession(self.server.domain, \
self.server.port, \
self.server.useTor)
msg=htmlPostReplies(self.server.translate, \
self.server.baseDir, \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
repliesJson, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(repliesJson).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
if self.path.endswith('/roles') and '/users/' in self.path:
namedStatus=self.path.split('/users/')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
nickname=postSections[0]
actorFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'.json'
if os.path.isfile(actorFilename):
actorJson=loadJson(actorFilename)
if actorJson:
if actorJson.get('roles'):
if self._requestHTTP():
getPerson = \
personLookup(self.server.domain, \
self.path.replace('/roles',''), \
self.server.baseDir)
if getPerson:
msg=htmlProfile(self.server.translate, \
self.server.projectVersion, \
self.server.baseDir, \
self.server.httpPrefix, \
True, \
self.server.ocapAlways, \
getPerson,'roles', \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
actorJson['roles'], \
None,None).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(actorJson['roles']).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
# show skills on the profile page
if self.path.endswith('/skills') and '/users/' in self.path:
namedStatus=self.path.split('/users/')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
nickname=postSections[0]
actorFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'.json'
if os.path.isfile(actorFilename):
actorJson=loadJson(actorFilename)
if actorJson:
if actorJson.get('skills'):
if self._requestHTTP():
getPerson = \
personLookup(self.server.domain, \
self.path.replace('/skills',''), \
self.server.baseDir)
if getPerson:
msg=htmlProfile(self.server.translate, \
self.server.projectVersion, \
self.server.baseDir, \
self.server.httpPrefix, \
True, \
self.server.ocapAlways, \
getPerson,'skills', \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
actorJson['skills'], \
None,None).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(actorJson['skills']).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
actor=self.path.replace('/skills','')
self._redirect_headers(actor,cookie)
self.server.GETbusy=False
return
# get an individual post from the path /users/nickname/statuses/number
if '/statuses/' in self.path and '/users/' in self.path:
namedStatus=self.path.split('/users/')[1]
if '/' in namedStatus:
postSections=namedStatus.split('/')
if len(postSections)>=3:
nickname=postSections[0]
statusNumber=postSections[2]
if len(statusNumber)>10 and statusNumber.isdigit():
postFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/outbox/'+ \
self.server.httpPrefix+':##'+ \
self.server.domainFull+'#users#'+ \
nickname+'#statuses#'+statusNumber+'.json'
if os.path.isfile(postFilename):
postJsonObject=loadJson(postFilename)
if not postJsonObject:
self.send_response(429)
self.end_headers()
self.server.GETbusy=False
return
else:
# Only authorized viewers get to see likes on posts
# Otherwize marketers could gain more social graph info
if not authorized:
if postJsonObject.get('likes'):
postJsonObject['likes']={'items': []}
if self._requestHTTP():
msg=htmlIndividualPost(self.server.translate, \
self.server.baseDir, \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
authorized,postJsonObject, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(postJsonObject).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
else:
self._404()
self.server.GETbusy=False
return
# get the inbox for a given person
if self.path.endswith('/inbox') or '/inbox?page=' in self.path:
if '/users/' in self.path:
if authorized:
inboxFeed=personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'inbox', \
True,self.server.ocapAlways)
if inboxFeed:
if self._requestHTTP():
nickname=self.path.replace('/users/','').replace('/inbox','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
if 'page=' not in self.path:
# if no page was specified then show the first
inboxFeed=personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=1', \
self.server.httpPrefix, \
maxPostsInFeed, 'inbox', \
True,self.server.ocapAlways)
msg=htmlInbox(self.server.translate, \
pageNumber,maxPostsInFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
inboxFeed, \
self.server.allowDeletion, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
# don't need authenticated fetch here because there is
# already the authorization check
msg=json.dumps(inboxFeed).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
self.server.GETbusy=False
return
else:
if self.server.debug:
nickname=self.path.replace('/users/','').replace('/inbox','')
print('DEBUG: '+nickname+ \
' was not authorized to access '+self.path)
if self.path!='/inbox':
# not the shared inbox
if self.server.debug:
print('DEBUG: GET access to inbox is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy=False
return
# get the direct messages for a given person
if self.path.endswith('/dm') or '/dm?page=' in self.path:
if '/users/' in self.path:
if authorized:
inboxDMFeed=personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'dm', \
True,self.server.ocapAlways)
if inboxDMFeed:
if self._requestHTTP():
nickname=self.path.replace('/users/','').replace('/dm','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
if 'page=' not in self.path:
# if no page was specified then show the first
inboxDMFeed=personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=1', \
self.server.httpPrefix, \
maxPostsInFeed, 'dm', \
True,self.server.ocapAlways)
msg=htmlInboxDMs(self.server.translate, \
pageNumber,maxPostsInFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
inboxDMFeed, \
self.server.allowDeletion, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
# don't need authenticated fetch here because there is
# already the authorization check
msg=json.dumps(inboxDMFeed).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
self.server.GETbusy=False
return
else:
if self.server.debug:
nickname=self.path.replace('/users/','').replace('/dm','')
print('DEBUG: '+nickname+ \
' was not authorized to access '+self.path)
if self.path!='/dm':
# not the DM inbox
if self.server.debug:
print('DEBUG: GET access to inbox is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy=False
return
# get the replies for a given person
if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path:
if '/users/' in self.path:
if authorized:
inboxRepliesFeed= \
personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'tlreplies', \
True,self.server.ocapAlways)
if not inboxRepliesFeed:
inboxRepliesFeed=[]
if self._requestHTTP():
nickname=self.path.replace('/users/','').replace('/tlreplies','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
if 'page=' not in self.path:
# if no page was specified then show the first
inboxRepliesFeed= \
personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=1', \
self.server.httpPrefix, \
maxPostsInFeed, 'tlreplies', \
True,self.server.ocapAlways)
msg=htmlInboxReplies(self.server.translate, \
pageNumber,maxPostsInFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
inboxRepliesFeed, \
self.server.allowDeletion, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
# don't need authenticated fetch here because there is
# already the authorization check
msg=json.dumps(inboxRepliesFeed).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
self.server.GETbusy=False
return
else:
if self.server.debug:
nickname=self.path.replace('/users/','').replace('/tlreplies','')
print('DEBUG: '+nickname+ \
' was not authorized to access '+self.path)
if self.path!='/tlreplies':
# not the replies inbox
if self.server.debug:
print('DEBUG: GET access to inbox is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy=False
return
# get the media for a given person
if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path:
if '/users/' in self.path:
if authorized:
inboxMediaFeed= \
personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path, \
self.server.httpPrefix, \
maxPostsInMediaFeed, 'tlmedia', \
True,self.server.ocapAlways)
if not inboxMediaFeed:
inboxMediaFeed=[]
if self._requestHTTP():
nickname=self.path.replace('/users/','').replace('/tlmedia','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
if 'page=' not in self.path:
# if no page was specified then show the first
inboxMediaFeed= \
personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=1', \
self.server.httpPrefix, \
maxPostsInMediaFeed, 'tlmedia', \
True,self.server.ocapAlways)
msg=htmlInboxMedia(self.server.translate, \
pageNumber,maxPostsInMediaFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
inboxMediaFeed, \
self.server.allowDeletion, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
# don't need authenticated fetch here because there is
# already the authorization check
msg=json.dumps(inboxMediaFeed).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
self.server.GETbusy=False
return
else:
if self.server.debug:
nickname=self.path.replace('/users/','').replace('/tlmedia','')
print('DEBUG: '+nickname+ \
' was not authorized to access '+self.path)
if self.path!='/tlmedia':
# not the media inbox
if self.server.debug:
print('DEBUG: GET access to inbox is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy=False
return
# get the shared items timeline for a given person
if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path:
if '/users/' in self.path:
if authorized:
if self._requestHTTP():
nickname=self.path.replace('/users/','').replace('/tlshares','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
msg=htmlShares(self.server.translate, \
pageNumber,maxPostsInFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
self.server.allowDeletion, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
# not the shares timeline
if self.server.debug:
print('DEBUG: GET access to shares timeline is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy=False
return
# get outbox feed for a person
outboxFeed=personBoxJson(self.server.session, \
self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'outbox', \
authorized, \
self.server.ocapAlways)
if outboxFeed:
if self._requestHTTP():
nickname= \
self.path.replace('/users/','').replace('/outbox','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
if 'page=' not in self.path:
# if a page wasn't specified then show the first one
outboxFeed=personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=1', \
self.server.httpPrefix, \
maxPostsInFeed, 'outbox', \
authorized, \
self.server.ocapAlways)
msg=htmlOutbox(self.server.translate, \
pageNumber,maxPostsInFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
outboxFeed, \
self.server.allowDeletion, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(outboxFeed).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
# get the moderation feed for a moderator
if self.path.endswith('/moderation') or \
'/moderation?page=' in self.path:
if '/users/' in self.path:
if authorized:
moderationFeed= \
personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path, \
self.server.httpPrefix, \
maxPostsInFeed, 'moderation', \
True,self.server.ocapAlways)
if moderationFeed:
if self._requestHTTP():
nickname= \
self.path.replace('/users/','').replace('/moderation','')
pageNumber=1
if '?page=' in nickname:
pageNumber=nickname.split('?page=')[1]
nickname=nickname.split('?page=')[0]
if pageNumber.isdigit():
pageNumber=int(pageNumber)
else:
pageNumber=1
if 'page=' not in self.path:
# if no page was specified then show the first
moderationFeed= \
personBoxJson(self.server.session, \
self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=1', \
self.server.httpPrefix, \
maxPostsInFeed, 'moderation', \
True,self.server.ocapAlways)
msg=htmlModeration(self.server.translate, \
pageNumber,maxPostsInFeed, \
self.server.session, \
self.server.baseDir, \
self.server.cachedWebfingers, \
self.server.personCache, \
nickname, \
self.server.domain, \
self.server.port, \
moderationFeed, \
True, \
self.server.httpPrefix, \
self.server.projectVersion).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
# don't need authenticated fetch here because there is
# already the authorization check
msg=json.dumps(moderationFeed).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
self.server.GETbusy=False
return
else:
if self.server.debug:
nickname=self.path.replace('/users/','').replace('/moderation','')
print('DEBUG: '+nickname+ \
' was not authorized to access '+self.path)
if self.server.debug:
print('DEBUG: GET access to moderation feed is unauthorized')
self.send_response(405)
self.end_headers()
self.server.GETbusy=False
return
shares=getSharesFeedForPerson(self.server.baseDir, \
self.server.domain, \
self.server.port,self.path, \
self.server.httpPrefix, \
sharesPerPage)
if shares:
if self._requestHTTP():
pageNumber=1
if '?page=' not in self.path:
searchPath=self.path
# get a page of shares, not the summary
shares= \
getSharesFeedForPerson(self.server.baseDir, \
self.server.domain, \
self.server.port, \
self.path+'?page=true', \
self.server.httpPrefix, \
sharesPerPage)
else:
pageNumberStr=self.path.split('?page=')[1]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
searchPath=self.path.split('?page=')[0]
getPerson= \
personLookup(self.server.domain, \
searchPath.replace('/shares',''), \
self.server.baseDir)
if getPerson:
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session')
self.server.session= \
createSession(self.server.domain, \
self.server.port, \
self.server.useTor)
msg=htmlProfile(self.server.translate, \
self.server.projectVersion, \
self.server.baseDir, \
self.server.httpPrefix, \
authorized, \
self.server.ocapAlways, \
getPerson,'shares', \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
shares, \
pageNumber,sharesPerPage).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
else:
if self._fetchAuthenticated():
msg=json.dumps(shares).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
following=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.httpPrefix, \
authorized,followsPerPage)
if following:
if self._requestHTTP():
pageNumber=1
if '?page=' not in self.path:
searchPath=self.path
# get a page of following, not the summary
following=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path+'?page=true', \
self.server.httpPrefix, \
authorized,followsPerPage)
else:
pageNumberStr=self.path.split('?page=')[1]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
searchPath=self.path.split('?page=')[0]
getPerson = personLookup(self.server.domain,searchPath.replace('/following',''), \
self.server.baseDir)
if getPerson:
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session')
self.server.session= \
createSession(self.server.domain, \
self.server.port, \
self.server.useTor)
msg=htmlProfile(self.server.translate, \
self.server.projectVersion, \
self.server.baseDir, \
self.server.httpPrefix, \
authorized, \
self.server.ocapAlways, \
getPerson,'following', \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
following, \
pageNumber,followsPerPage).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
else:
if self._fetchAuthenticated():
msg=json.dumps(following).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path, \
self.server.httpPrefix, \
authorized,followsPerPage,'followers')
if followers:
if self._requestHTTP():
pageNumber=1
if '?page=' not in self.path:
searchPath=self.path
# get a page of followers, not the summary
followers= \
getFollowingFeed(self.server.baseDir,self.server.domain, \
self.server.port,self.path+'?page=1', \
self.server.httpPrefix, \
authorized,followsPerPage,'followers')
else:
pageNumberStr=self.path.split('?page=')[1]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
searchPath=self.path.split('?page=')[0]
getPerson= \
personLookup(self.server.domain,searchPath.replace('/followers',''), \
self.server.baseDir)
if getPerson:
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session')
self.server.session= \
createSession(self.server.domain, \
self.server.port,self.server.useTor)
msg=htmlProfile(self.server.translate, \
self.server.projectVersion, \
self.server.baseDir, \
self.server.httpPrefix, \
authorized, \
self.server.ocapAlways, \
getPerson,'followers', \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
followers, \
pageNumber,followsPerPage).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.GETbusy=False
return
else:
if self._fetchAuthenticated():
msg=json.dumps(followers).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
# look up a person
getPerson = personLookup(self.server.domain,self.path, \
self.server.baseDir)
if getPerson:
if self._requestHTTP():
if not self.server.session:
if self.server.debug:
print('DEBUG: creating new session')
self.server.session= \
createSession(self.server.domain, \
self.server.port,self.server.useTor)
msg=htmlProfile(self.server.translate, \
self.server.projectVersion, \
self.server.baseDir, \
self.server.httpPrefix, \
authorized, \
self.server.ocapAlways, \
getPerson,'posts',
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
None,None).encode('utf-8')
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
else:
if self._fetchAuthenticated():
msg=json.dumps(getPerson).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
self._404()
self.server.GETbusy=False
return
# check that a json file was requested
if not self.path.endswith('.json'):
if self.server.debug:
print('DEBUG: GET Not json: '+self.path+' '+self.server.baseDir)
self._404()
self.server.GETbusy=False
return
if not self._fetchAuthenticated():
if self.server.debug:
print('WARN: Unauthenticated GET')
self._404()
# check that the file exists
filename=self.server.baseDir+self.path
if os.path.isfile(filename):
with open(filename, 'r', encoding='utf-8') as File:
content = File.read()
contentJson=json.loads(content)
msg=json.dumps(contentJson).encode('utf-8')
self._set_headers('application/json',len(msg),None)
self._write(msg)
else:
if self.server.debug:
print('DEBUG: GET Unknown file')
self._404()
self.server.GETbusy=False
def do_HEAD(self):
self._set_headers('application/json',0,None)
def _receiveNewPostProcess(self,authorized: bool, \
postType: str,path: str,headers: {}) -> int:
# Note: this needs to happen synchronously
# 0 = this is not a new post
# 1 = new post success
# -1 = new post failed
# 2 = new post canceled
if ' boundary=' in headers['Content-Type']:
nickname=None
nicknameStr=path.split('/users/')[1]
if '/' in nicknameStr:
nickname=nicknameStr.split('/')[0]
else:
return -1
length = int(headers['Content-Length'])
if length>self.server.maxPostLength:
print('POST size too large')
return -1
boundary=headers['Content-Type'].split('boundary=')[1]
if ';' in boundary:
boundary=boundary.split(';')[0]
# Note: we don't use cgi here because it's due to be deprecated
# in Python 3.8/3.10
# Instead we use the multipart mime parser from the email module
postBytes=self.rfile.read(length)
msg = email.parser.BytesParser().parsebytes(postBytes)
# why don't we just use msg.is_multipart(), rather than splitting?
# TL;DR it doesn't work for this use case because we're not using
# email style encoding message/rfc822
messageFields=msg.get_payload(decode=False).split(boundary)
fields={}
filename=None
attachmentMediaType=None
for f in messageFields:
if f=='--':
continue
if ' name="' in f:
postStr=f.split(' name="',1)[1]
if '"' in postStr:
postKey=postStr.split('"',1)[0]
postValueStr=postStr.split('"',1)[1]
if ';' not in postValueStr:
if '\r\n' in postValueStr:
postLines=postValueStr.split('\r\n')
postValue=''
if len(postLines)>2:
for line in range(2,len(postLines)-1):
if line>2:
postValue+='\n'
postValue+=postLines[line]
fields[postKey]=postValue
else:
# directly search the binary array for the beginning
# of an image
extensionList=['png','jpeg','gif','mp4','webm','ogv','mp3','ogg']
for extension in extensionList:
searchStr=b'Content-Type: image/png'
if extension=='jpeg':
searchStr=b'Content-Type: image/jpeg'
elif extension=='gif':
searchStr=b'Content-Type: image/gif'
elif extension=='mp4':
searchStr=b'Content-Type: video/mp4'
elif extension=='ogv':
searchStr=b'Content-Type: video/ogv'
elif extension=='mp3':
searchStr=b'Content-Type: audio/mpeg'
elif extension=='ogg':
searchStr=b'Content-Type: audio/ogg'
imageLocation=postBytes.find(searchStr)
filenameBase= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/upload'
if imageLocation>-1:
if extension=='jpeg':
extension='jpg'
if extension=='mpeg':
extension='mp3'
filename=filenameBase+'.'+extension
attachmentMediaType= \
searchStr.decode().split('/')[0].replace('Content-Type: ','')
break
if filename and imageLocation>-1:
# locate the beginning of the image, after any
# carriage returns
startPos=imageLocation+len(searchStr)
for offset in range(1,8):
if postBytes[startPos+offset]!=10:
if postBytes[startPos+offset]!=13:
startPos+=offset
break
fd = open(filename, 'wb')
fd.write(postBytes[startPos:])
fd.close()
else:
filename=None
# send the post
if not fields.get('message') and not fields.get('imageDescription'):
return -1
if fields.get('submitPost'):
if fields['submitPost']!='Submit':
return -1
else:
return 2
if not fields.get('imageDescription'):
fields['imageDescription']=None
if not fields.get('subject'):
fields['subject']=None
if not fields.get('replyTo'):
fields['replyTo']=None
if not fields.get('eventDate'):
fields['eventDate']=None
if not fields.get('eventTime'):
fields['eventTime']=None
if not fields.get('location'):
fields['location']=None
if postType=='newpost':
messageJson= \
createPublicPost(self.server.baseDir, \
nickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
fields['message'],False,False,False, \
filename,attachmentMediaType, \
fields['imageDescription'],True, \
fields['replyTo'],fields['replyTo'], \
fields['subject'], \
fields['eventDate'],fields['eventTime'], \
fields['location'])
if messageJson:
self.postToNickname=nickname
if self._postToOutbox(messageJson,__version__):
populateReplies(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domainFull, \
messageJson, \
self.server.maxReplies, \
self.server.debug)
return 1
else:
return -1
if postType=='newunlisted':
messageJson= \
createUnlistedPost(self.server.baseDir, \
nickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
fields['message'],False,False,False, \
filename,attachmentMediaType, \
fields['imageDescription'],True, \
fields['replyTo'], fields['replyTo'], \
fields['subject'], \
fields['eventDate'],fields['eventTime'], \
fields['location'])
if messageJson:
self.postToNickname=nickname
if self._postToOutbox(messageJson,__version__):
populateReplies(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domain, \
messageJson, \
self.server.maxReplies, \
self.server.debug)
return 1
else:
return -1
if postType=='newfollowers':
messageJson= \
createFollowersOnlyPost(self.server.baseDir, \
nickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
fields['message'],True,False,False, \
filename,attachmentMediaType, \
fields['imageDescription'],True, \
fields['replyTo'], fields['replyTo'], \
fields['subject'], \
fields['eventDate'],fields['eventTime'], \
fields['location'])
if messageJson:
self.postToNickname=nickname
if self._postToOutbox(messageJson,__version__):
populateReplies(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domain, \
messageJson, \
self.server.maxReplies, \
self.server.debug)
return 1
else:
return -1
if postType=='newdm':
messageJson=None
if '@' in fields['message']:
messageJson= \
createDirectMessagePost(self.server.baseDir, \
nickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
fields['message'],True,False,False, \
filename,attachmentMediaType, \
fields['imageDescription'],True, \
fields['replyTo'],fields['replyTo'], \
fields['subject'], \
self.server.debug, \
fields['eventDate'], \
fields['eventTime'], \
fields['location'])
if messageJson:
self.postToNickname=nickname
if self.server.debug:
print('DEBUG: new DM to '+str(messageJson['object']['to']))
if self._postToOutbox(messageJson,__version__):
populateReplies(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domain, \
messageJson, \
self.server.maxReplies, \
self.server.debug)
return 1
else:
return -1
if postType=='newreport':
if attachmentMediaType:
if attachmentMediaType!='image':
return -1
# So as to be sure that this only goes to moderators
# and not accounts being reported we disable any
# included fediverse addresses by replacing '@' with '-at-'
fields['message']=fields['message'].replace('@','-at-')
messageJson= \
createReportPost(self.server.baseDir, \
nickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
fields['message'],True,False,False, \
filename,attachmentMediaType, \
fields['imageDescription'],True, \
self.server.debug,fields['subject'])
if messageJson:
self.postToNickname=nickname
if self._postToOutbox(messageJson,__version__):
return 1
else:
return -1
if postType=='newshare':
if not fields.get('itemType'):
return -1
if not fields.get('category'):
return -1
if not fields.get('location'):
return -1
if not fields.get('duration'):
return -1
if attachmentMediaType:
if attachmentMediaType!='image':
return -1
durationStr=fields['duration']
if durationStr:
if ' ' not in durationStr:
durationStr=durationStr+' days'
addShare(self.server.baseDir, \
self.server.httpPrefix, \
nickname, \
self.server.domain,self.server.port, \
fields['subject'], \
fields['message'], \
filename, \
fields['itemType'], \
fields['category'], \
fields['location'], \
durationStr,
self.server.debug)
if filename:
if os.path.isfile(filename):
os.remove(filename)
self.postToNickname=nickname
return 1
return -1
def _receiveNewPost(self,authorized: bool,postType: str,path: str) -> int:
"""A new post has been created
This creates a thread to send the new post
"""
pageNumber=1
if not (authorized and '/users/' in path and '?'+postType+'?' in path):
print('Not receiving new post for '+path)
return None
print('New post begins: '+postType+' '+path)
if '?page=' in path:
pageNumberStr=path.split('?page=')[1]
if '?' in pageNumberStr:
pageNumberStr=pageNumberStr.split('?')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
path=path.split('?page=')[0]
newPostThreadName=self.postToNickname
if not newPostThreadName:
newPostThreadName='*'
if self.server.newPostThread.get(newPostThreadName):
print('Waiting for previous new post thread to end')
waitCtr=0
while self.server.newPostThread[newPostThreadName].isAlive() and waitCtr<8:
time.sleep(1)
waitCtr+=1
if waitCtr>=8:
self.server.newPostThread[newPostThreadName].kill()
# make a copy of self.headers
headers={}
for dictEntryName,headerLine in self.headers.items():
headers[dictEntryName]=headerLine
print('New post headers: '+str(headers))
# Note sending new posts needs to be synchronous, otherwise any attachments
# can get mangled if other events happen during their decoding
print('Creating new post: '+newPostThreadName)
self._receiveNewPostProcess(authorized,postType,path,headers)
return pageNumber
def do_POST(self):
if not self.server.session:
self.server.session= \
createSession(self.server.domain,self.server.port, \
self.server.useTor)
if self.server.debug:
print('DEBUG: POST to from '+self.server.baseDir+ \
' path: '+self.path+' busy: '+ \
str(self.server.POSTbusy))
if self.server.POSTbusy:
currTimePOST=int(time.time())
if currTimePOST-self.server.lastPOST==0:
self.send_response(429)
self.end_headers()
return
self.server.lastPOST=currTimePOST
self.server.POSTbusy=True
if not self.headers.get('Content-type'):
print('Content-type header missing')
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
# remove any trailing slashes from the path
if not self.path.endswith('confirm'):
self.path= \
self.path.replace('/outbox/','/outbox').replace('/inbox/','/inbox').replace('/shares/','/shares').replace('/sharedInbox/','/sharedInbox')
cookie=None
if self.headers.get('Cookie'):
cookie=self.headers['Cookie']
# check authorization
authorized = self._isAuthorized()
if authorized:
if self.server.debug:
print('POST Authorization granted')
else:
if self.server.debug:
print('POST Not authorized')
print(str(self.headers))
# if this is a POST to teh outbox then check authentication
self.outboxAuthenticated=False
self.postToNickname=None
if self.path.startswith('/login'):
# get the contents of POST containing login credentials
length = int(self.headers['Content-length'])
if length>512:
print('Login failed - credentials too long')
self.send_response(401)
self.end_headers()
self.server.POSTbusy=False
return
loginParams=self.rfile.read(length).decode('utf-8')
loginNickname,loginPassword,register= \
htmlGetLoginCredentials(loginParams,self.server.lastLoginTime)
if loginNickname:
self.server.lastLoginTime=int(time.time())
if register:
if not registerAccount(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domain, \
self.server.port, \
loginNickname,loginPassword):
self.server.POSTbusy=False
self._redirect_headers('/login',cookie)
return
authHeader=createBasicAuthHeader(loginNickname,loginPassword)
if not authorizeBasic(self.server.baseDir,'/users/'+ \
loginNickname+'/outbox',authHeader,False):
print('Login failed: '+loginNickname)
self._clearLoginDetails(loginNickname)
self.server.POSTbusy=False
return
else:
if isSuspended(self.server.baseDir,loginNickname):
msg=htmlSuspended(self.server.baseDir).encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
# login success - redirect with authorization
print('Login success: '+loginNickname)
self.send_response(303)
# re-activate account if needed
activateAccount(self.server.baseDir,loginNickname,self.server.domain)
# This produces a deterministic token based on nick+password+salt
saltFilename= \
self.server.baseDir+'/accounts/'+ \
loginNickname+'@'+self.server.domain+'/.salt'
salt=createPassword(32)
if os.path.isfile(saltFilename):
try:
with open(saltFilename, 'r') as fp:
salt = fp.read()
except Exception as e:
print('WARN: Unable to read salt for '+ \
loginNickname+' '+str(e))
else:
try:
with open(saltFilename, 'w') as fp:
fp.write(salt)
except Exception as e:
print('WARN: Unable to save salt for '+ \
loginNickname+' '+str(e))
token=sha256((loginNickname+loginPassword+salt).encode('utf-8')).hexdigest()
self.server.tokens[loginNickname]=token
tokenFilename= \
self.server.baseDir+'/accounts/'+ \
loginNickname+'@'+self.server.domain+'/.token'
try:
with open(tokenFilename, 'w') as fp:
fp.write(token)
except Exception as e:
print('WARN: Unable to save token for '+loginNickname+' '+str(e))
self.server.tokensLookup[self.server.tokens[loginNickname]]=loginNickname
self.send_header('Set-Cookie', \
'epicyon='+self.server.tokens[loginNickname]+'; SameSite=Strict')
self.send_header('Location', \
'/users/'+loginNickname+'/inbox')
self.send_header('Content-Length', '0')
self.send_header('X-Robots-Tag','noindex')
self.end_headers()
self.server.POSTbusy=False
return
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
return
# update of profile/avatar from web interface
if authorized and self.path.endswith('/profiledata'):
if ' boundary=' in self.headers['Content-type']:
boundary=self.headers['Content-type'].split('boundary=')[1]
if ';' in boundary:
boundary=boundary.split(';')[0]
actorStr=self.path.replace('/profiledata','').replace('/editprofile','')
nickname=getNicknameFromActor(actorStr)
if not nickname:
print('WARN: nickname not found in '+actorStr)
self._redirect_headers(actorStr,cookie)
self.server.POSTbusy=False
return
length = int(self.headers['Content-length'])
if length>self.server.maxPostLength:
print('Maximum profile data length exceeded '+str(length))
self._redirect_headers(actorStr,cookie)
self.server.POSTbusy=False
return
postBytes=self.rfile.read(length)
msg = email.parser.BytesParser().parsebytes(postBytes)
messageFields=msg.get_payload(decode=False).split(boundary)
fields={}
filename=None
lastImageLocation=0
for f in messageFields:
if f=='--':
continue
if ' name="' in f:
postStr=f.split(' name="',1)[1]
if '"' in postStr:
postKey=postStr.split('"',1)[0]
postValueStr=postStr.split('"',1)[1]
if ';' not in postValueStr:
if '\r\n' in postValueStr:
postLines=postValueStr.split('\r\n')
postValue=''
if len(postLines)>2:
for line in range(2,len(postLines)-1):
if line>2:
postValue+='\n'
postValue+=postLines[line]
fields[postKey]=postValue
else:
if 'filename="' not in postStr:
continue
filenameStr=postStr.split('filename="')[1]
if '"' not in filenameStr:
continue
postImageFilename=filenameStr.split('"')[0]
if '.' not in postImageFilename:
continue
# directly search the binary array for the beginning
# of an image
searchStr=b'Content-Type: image/png'
imageLocation=postBytes.find(searchStr,lastImageLocation)
filenameBase= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/'+postKey
# Note: a .temp extension is used here so that at no time is
# an image with metadata publicly exposed, even for a few mS
if imageLocation>-1:
filename=filenameBase+'.png.temp'
else:
searchStr=b'Content-Type: image/jpeg'
imageLocation= \
postBytes.find(searchStr, \
lastImageLocation)
if imageLocation>-1:
filename=filenameBase+'.jpg.temp'
else:
searchStr=b'Content-Type: image/gif'
imageLocation= \
postBytes.find(searchStr, \
lastImageLocation)
if imageLocation>-1:
filename=filenameBase+'.gif.temp'
if filename and imageLocation>-1:
# locate the beginning of the image, after any
# carriage returns
startPos=imageLocation+len(searchStr)
for offset in range(1,8):
if postBytes[startPos+offset]!=10:
if postBytes[startPos+offset]!=13:
startPos+=offset
break
# look for the end of the image
imageLocationEnd= \
postBytes.find(b'-------',imageLocation+1)
fd = open(filename, 'wb')
if imageLocationEnd>-1:
fd.write(postBytes[startPos:][:imageLocationEnd-startPos])
else:
fd.write(postBytes[startPos:])
fd.close()
# remove exif/metadata
removeMetaData(filename, \
filename.replace('.temp',''))
os.remove(filename)
lastImageLocation=imageLocation+1
actorFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'.json'
if os.path.isfile(actorFilename):
actorJson=loadJson(actorFilename)
if actorJson:
actorChanged=False
skillCtr=1
newSkills={}
while skillCtr<10:
skillName=fields.get('skillName'+str(skillCtr))
if not skillName:
skillCtr+=1
continue
skillValue=fields.get('skillValue'+str(skillCtr))
if not skillValue:
skillCtr+=1
continue
if not actorJson['skills'].get(skillName):
actorChanged=True
else:
if actorJson['skills'][skillName]!=int(skillValue):
actorChanged=True
newSkills[skillName]=int(skillValue)
skillCtr+=1
if len(actorJson['skills'].items())!=len(newSkills.items()):
actorChanged=True
actorJson['skills']=newSkills
if fields.get('displayNickname'):
if fields['displayNickname']!=actorJson['name']:
actorJson['name']=fields['displayNickname']
actorChanged=True
if fields.get('bio'):
if fields['bio']!=actorJson['summary']:
actorTags={}
actorJson['summary']= \
addHtmlTags(self.server.baseDir, \
self.server.httpPrefix, \
nickname, \
self.server.domainFull, \
fields['bio'],[],actorTags)
if actorTags:
actorJson['tag']=[]
for tagName,tag in actorTags.items():
actorJson['tag'].append(tag)
actorChanged=True
if fields.get('moderators'):
adminNickname=getConfigParam(self.server.baseDir,'admin')
if self.path.startswith('/users/'+adminNickname+'/'):
moderatorsFile=self.server.baseDir+'/accounts/moderators.txt'
clearModeratorStatus(self.server.baseDir)
if ',' in fields['moderators']:
# if the list was given as comma separated
modFile=open(moderatorsFile,"w+")
for modNick in fields['moderators'].split(','):
modNick=modNick.strip()
if os.path.isdir(self.server.baseDir+ \
'/accounts/'+modNick+ \
'@'+self.server.domain):
modFile.write(modNick+'\n')
modFile.close()
for modNick in fields['moderators'].split(','):
modNick=modNick.strip()
if os.path.isdir(self.server.baseDir+ \
'/accounts/'+modNick+ \
'@'+self.server.domain):
setRole(self.server.baseDir, \
modNick,self.server.domain, \
'instance','moderator')
else:
# nicknames on separate lines
modFile=open(moderatorsFile,"w+")
for modNick in fields['moderators'].split('\n'):
modNick=modNick.strip()
if os.path.isdir(self.server.baseDir+ \
'/accounts/'+modNick+ \
'@'+self.server.domain):
modFile.write(modNick+'\n')
modFile.close()
for modNick in fields['moderators'].split('\n'):
modNick=modNick.strip()
if os.path.isdir(self.server.baseDir+ \
'/accounts/'+modNick+ \
'@'+self.server.domain):
setRole(self.server.baseDir, \
modNick,self.server.domain, \
'instance','moderator')
approveFollowers=False
if fields.get('approveFollowers'):
if fields['approveFollowers']=='on':
approveFollowers=True
if approveFollowers!=actorJson['manuallyApprovesFollowers']:
actorJson['manuallyApprovesFollowers']=approveFollowers
actorChanged=True
# only receive DMs from accounts you follow
followDMsFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/.followDMs'
followDMsActive=False
if fields.get('followDMs'):
if fields['followDMs']=='on':
followDMsActive=True
with open(followDMsFilename, "w") as followDMsFile:
followDMsFile.write('\n')
if not followDMsActive:
if os.path.isfile(followDMsFilename):
os.remove(followDMsFilename)
# this account is a bot
if fields.get('isBot'):
if fields['isBot']=='on':
if actorJson['type']!='Service':
actorJson['type']='Service'
actorChanged=True
else:
# this account is a group
if fields.get('isGroup'):
if fields['isGroup']=='on':
if actorJson['type']!='Group':
actorJson['type']='Group'
actorChanged=True
else:
# this account is a person (default)
if actorJson['type']!='Person':
actorJson['type']='Person'
actorChanged=True
# save filtered words list
filterFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/filters.txt'
if fields.get('filteredWords'):
with open(filterFilename, "w") as filterfile:
filterfile.write(fields['filteredWords'])
else:
if os.path.isfile(filterFilename):
os.remove(filterFilename)
# save blocked accounts list
blockedFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/blocking.txt'
if fields.get('blocked'):
with open(blockedFilename, "w") as blockedfile:
blockedfile.write(fields['blocked'])
else:
if os.path.isfile(blockedFilename):
os.remove(blockedFilename)
# save allowed instances list
allowedInstancesFilename= \
self.server.baseDir+'/accounts/'+ \
nickname+'@'+self.server.domain+'/allowedinstances.txt'
if fields.get('allowedInstances'):
with open(allowedInstancesFilename, "w") as allowedInstancesFile:
allowedInstancesFile.write(fields['allowedInstances'])
else:
if os.path.isfile(allowedInstancesFilename):
os.remove(allowedInstancesFilename)
# save actor json file within accounts
if actorChanged:
saveJson(actorJson,actorFilename)
# also copy to the actors cache and personCache in memory
storePersonInCache(self.server.baseDir, \
actorJson['id'],actorJson, \
self.server.personCache)
actorCacheFilename= \
self.server.baseDir+'/cache/actors/'+ \
actorJson['id'].replace('/','#')+'.json'
saveJson(actorJson,actorCacheFilename)
# send actor update to followers
updateActorJson={
'type': 'Update',
'actor': actorJson['id'],
'to': ['https://www.w3.org/ns/activitystreams#Public'],
'cc': [actorJson['id']+'/followers'],
'object': actorJson
}
self.postToNickname=nickname
self._postToOutboxThread(updateActorJson)
if fields.get('deactivateThisAccount'):
if fields['deactivateThisAccount']=='on':
deactivateAccount(self.server.baseDir,nickname,self.server.domain)
self._clearLoginDetails(nickname)
self.server.POSTbusy=False
return
self._redirect_headers(actorStr,cookie)
self.server.POSTbusy=False
return
# moderator action buttons
if authorized and '/users/' in self.path and \
self.path.endswith('/moderationaction'):
actorStr=self.path.replace('/moderationaction','')
length = int(self.headers['Content-length'])
moderationParams=self.rfile.read(length).decode('utf-8')
print('moderationParams: '+moderationParams)
if '&' in moderationParams:
moderationText=None
moderationButton=None
for moderationStr in moderationParams.split('&'):
print('moderationStr: '+moderationStr)
if moderationStr.startswith('moderationAction'):
if '=' in moderationStr:
moderationText= \
moderationStr.split('=')[1].strip()
moderationText= \
moderationText.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').strip()
elif moderationStr.startswith('submitInfo'):
msg=htmlModerationInfo(self.server.translate, \
self.server.baseDir).encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
elif moderationStr.startswith('submitBlock'):
moderationButton='block'
elif moderationStr.startswith('submitUnblock'):
moderationButton='unblock'
elif moderationStr.startswith('submitSuspend'):
moderationButton='suspend'
elif moderationStr.startswith('submitUnsuspend'):
moderationButton='unsuspend'
elif moderationStr.startswith('submitRemove'):
moderationButton='remove'
if moderationButton and moderationText:
if self.server.debug:
print('moderationButton: '+moderationButton)
print('moderationText: '+moderationText)
nickname=moderationText
if nickname.startswith('http') or \
nickname.startswith('dat'):
nickname=getNicknameFromActor(nickname)
if '@' in nickname:
nickname=nickname.split('@')[0]
if moderationButton=='suspend':
suspendAccount(self.server.baseDir,nickname, \
self.server.domain)
if moderationButton=='unsuspend':
unsuspendAccount(self.server.baseDir,nickname)
if moderationButton=='block':
fullBlockDomain=None
if moderationText.startswith('http') or \
moderationText.startswith('dat'):
blockDomain,blockPort= \
getDomainFromActor(moderationText)
fullBlockDomain=blockDomain
if blockPort:
if blockPort!=80 and blockPort!=443:
if ':' not in blockDomain:
fullBlockDomain= \
blockDomain+':'+str(blockPort)
if '@' in moderationText:
fullBlockDomain=moderationText.split('@')[1]
if fullBlockDomain or nickname.startswith('#'):
addGlobalBlock(self.server.baseDir, \
nickname,fullBlockDomain)
if moderationButton=='unblock':
fullBlockDomain=None
if moderationText.startswith('http') or \
moderationText.startswith('dat'):
blockDomain,blockPort= \
getDomainFromActor(moderationText)
fullBlockDomain=blockDomain
if blockPort:
if blockPort!=80 and blockPort!=443:
if ':' not in blockDomain:
fullBlockDomain= \
blockDomain+':'+str(blockPort)
if '@' in moderationText:
fullBlockDomain=moderationText.split('@')[1]
if fullBlockDomain or nickname.startswith('#'):
removeGlobalBlock(self.server.baseDir, \
nickname,fullBlockDomain)
if moderationButton=='remove':
if '/statuses/' not in moderationText:
removeAccount(self.server.baseDir, \
nickname, \
self.server.domain, \
self.server.port)
else:
# remove a post or thread
postFilename= \
locatePost(self.server.baseDir, \
nickname,self.server.domain, \
moderationText)
if postFilename:
if canRemovePost(self.server.baseDir, \
nickname, \
self.server.domain, \
self.server.port, \
moderationText):
deletePost(self.server.baseDir, \
self.server.httpPrefix, \
nickname,self.server.omain, \
postFilename, \
self.server.debug)
self._redirect_headers(actorStr+'/moderation',cookie)
self.server.POSTbusy=False
return
searchForEmoji=False
if self.path.endswith('/searchhandleemoji'):
searchForEmoji=True
self.path=self.path.replace('/searchhandleemoji','/searchhandle')
if self.server.debug:
print('DEBUG: searching for emoji')
print('authorized: '+str(authorized))
# a vote/question/poll is posted
if authorized and \
(self.path.endswith('/question') or '/question?page=' in self.path):
pageNumber=1
if '?page=' in self.path:
pageNumberStr=self.path.split('?page=')[1]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
self.path=self.path.split('?page=')[0]
# the actor who votes
actor= \
self.server.httpPrefix+'://'+ \
self.server.domainFull+self.path.replace('/question','')
nickname=getNicknameFromActor(actor)
if not nickname:
self._redirect_headers(actor+'/inbox?page='+ \
str(pageNumber),cookie)
self.server.POSTbusy=False
return
# get the parameters
length = int(self.headers['Content-length'])
questionParams=self.rfile.read(length).decode('utf-8')
questionParams= \
questionParams.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').strip()
# post being voted on
messageId=None
if 'messageId=' in questionParams:
messageId=questionParams.split('messageId=')[1]
if '&' in messageId:
messageId=messageId.split('&')[0]
answer=None
if 'answer=' in questionParams:
answer=questionParams.split('answer=')[1]
if '&' in answer:
answer=answer.split('&')[0]
print('Voting on message '+messageId)
print('Vote for: '+answer)
messageJson= \
createPublicPost(self.server.baseDir, \
nickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
answer,False,False,False, \
None,None,None,True, \
messageId,messageId,None, \
None,None,None)
if messageJson:
self.postToNickname=nickname
if self._postToOutbox(messageJson,__version__):
populateReplies(self.server.baseDir, \
self.server.httpPrefix, \
self.server.domainFull, \
messageJson, \
self.server.maxReplies, \
self.server.debug)
self._redirect_headers(actor+'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
# a search was made
if (authorized or searchForEmoji) and \
(self.path.endswith('/searchhandle') or \
'/searchhandle?page=' in self.path):
# get the page number
pageNumber=1
if '/searchhandle?page=' in self.path:
pageNumberStr=self.path.split('/searchhandle?page=')[1]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
self.path=self.path.split('?page=')[0]
actorStr= \
self.server.httpPrefix+'://'+ \
self.server.domainFull+ \
self.path.replace('/searchhandle','')
length = int(self.headers['Content-length'])
searchParams=self.rfile.read(length).decode('utf-8')
if 'searchtext=' in searchParams:
searchStr=searchParams.split('searchtext=')[1]
if '&' in searchStr:
searchStr=searchStr.split('&')[0]
searchStr= \
searchStr.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').strip()
if self.server.debug:
print('searchStr: '+searchStr)
if searchForEmoji:
searchStr=':'+searchStr+':'
if searchStr.startswith('#'):
# hashtag search
hashtagStr= \
htmlHashtagSearch(self.server.translate, \
self.server.baseDir,searchStr[1:],1, \
maxPostsInFeed,self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.httpPrefix, \
self.server.projectVersion)
if hashtagStr:
msg=hashtagStr.encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
elif searchStr.startswith('*'):
# skill search
searchStr=searchStr.replace('*','').strip()
skillStr= \
htmlSkillsSearch(self.server.translate, \
self.server.baseDir,searchStr, \
self.server.instanceOnlySkillsSearch, \
64)
if skillStr:
msg=skillStr.encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
elif '@' in searchStr:
# profile search
nickname=getNicknameFromActor(self.path)
if not self.server.session:
self.server.session= \
createSession(self.server.domain, \
self.server.port, \
self.server.useTor)
profileStr= \
htmlProfileAfterSearch(self.server.translate, \
self.server.baseDir, \
self.path.replace('/searchhandle',''), \
self.server.httpPrefix, \
nickname, \
self.server.domain,self.server.port, \
searchStr, \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.debug, \
self.server.projectVersion)
if profileStr:
msg=profileStr.encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
else:
self._redirect_headers(actorStr+'/search',cookie)
self.server.POSTbusy=False
return
elif searchStr.startswith(':') or \
searchStr.lower().strip('\n').endswith(' emoji'):
# eg. "cat emoji"
if searchStr.lower().strip('\n').endswith(' emoji'):
searchStr= \
searchStr.lower().strip('\n').replace(' emoji','')
# emoji search
emojiStr= \
htmlSearchEmoji(self.server.translate, \
self.server.baseDir,searchStr)
if emojiStr:
msg=emojiStr.encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
else:
# shared items search
sharedItemsStr= \
htmlSearchSharedItems(self.server.translate, \
self.server.baseDir, \
searchStr,pageNumber, \
maxPostsInFeed, \
self.server.httpPrefix, \
self.server.domainFull, \
actorStr)
if sharedItemsStr:
msg=sharedItemsStr.encode('utf-8')
self._login_headers('text/html',len(msg))
self._write(msg)
self.server.POSTbusy=False
return
self._redirect_headers(actorStr+'/inbox',cookie)
self.server.POSTbusy=False
return
# removes a shared item
if authorized and self.path.endswith('/rmshare'):
originPathStr=self.path.split('/rmshare')[0]
length = int(self.headers['Content-length'])
removeShareConfirmParams=self.rfile.read(length).decode('utf-8')
if '&submitYes=' in removeShareConfirmParams:
removeShareConfirmParams= \
removeShareConfirmParams.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').replace('+',' ').strip()
shareActor=removeShareConfirmParams.split('actor=')[1]
if '&' in shareActor:
shareActor=shareActor.split('&')[0]
shareName=removeShareConfirmParams.split('shareName=')[1]
if '&' in shareName:
shareName=shareName.split('&')[0]
shareNickname=getNicknameFromActor(shareActor)
if shareNickname:
shareDomain,sharePort=getDomainFromActor(shareActor)
removeShare(self.server.baseDir, \
shareNickname,shareDomain,shareName)
self._redirect_headers(originPathStr+'/tlshares',cookie)
self.server.POSTbusy=False
return
# removes a post
if authorized and self.path.endswith('/rmpost'):
pageNumber=1
originPathStr=self.path.split('/rmpost')[0]
length = int(self.headers['Content-length'])
removePostConfirmParams=self.rfile.read(length).decode('utf-8')
if '&submitYes=' in removePostConfirmParams:
removePostConfirmParams= \
removePostConfirmParams.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').strip()
removeMessageId= \
removePostConfirmParams.split('messageId=')[1]
if '&' in removeMessageId:
removeMessageId=removeMessageId.split('&')[0]
if 'pageNumber=' in removePostConfirmParams:
pageNumberStr=removePostConfirmParams.split('pageNumber=')[1]
if '&' in pageNumberStr:
pageNumberStr=pageNumberStr.split('&')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
if '/statuses/' in removeMessageId:
removePostActor=removeMessageId.split('/statuses/')[0]
if originPathStr in removePostActor:
deleteJson= {
"@context": "https://www.w3.org/ns/activitystreams",
'actor': removePostActor,
'object': removeMessageId,
'to': ['https://www.w3.org/ns/activitystreams#Public',removePostActor],
'cc': [removePostActor+'/followers'],
'type': 'Delete'
}
if self.server.debug:
pprint(deleteJson)
self.postToNickname=getNicknameFromActor(removePostActor)
if self.postToNickname:
self._postToOutboxThread(deleteJson)
if pageNumber==1:
self._redirect_headers(originPathStr+'/outbox',cookie)
else:
self._redirect_headers(originPathStr+'/outbox?page='+ \
str(pageNumber),cookie)
self.server.POSTbusy=False
return
# decision to follow in the web interface is confirmed
if authorized and self.path.endswith('/followconfirm'):
originPathStr=self.path.split('/followconfirm')[0]
followerNickname=getNicknameFromActor(originPathStr)
length = int(self.headers['Content-length'])
followConfirmParams=self.rfile.read(length).decode('utf-8')
if '&submitView=' in followConfirmParams:
followingActor= \
followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
if '&' in followingActor:
followingActor=followingActor.split('&')[0]
self._redirect_headers(followingActor,cookie)
self.server.POSTbusy=False
return
if '&submitYes=' in followConfirmParams:
followingActor= \
followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
if '&' in followingActor:
followingActor=followingActor.split('&')[0]
followingNickname=getNicknameFromActor(followingActor)
followingDomain,followingPort=getDomainFromActor(followingActor)
if followerNickname==followingNickname and \
followingDomain==self.server.domain and \
followingPort==self.server.port:
if self.server.debug:
print('You cannot follow yourself!')
else:
if self.server.debug:
print('Sending follow request from '+ \
followerNickname+' to '+followingActor)
sendFollowRequest(self.server.session, \
self.server.baseDir, \
followerNickname, \
self.server.domain,self.server.port, \
self.server.httpPrefix, \
followingNickname, \
followingDomain, \
followingPort,self.server.httpPrefix, \
False,self.server.federationList, \
self.server.sendThreads, \
self.server.postLog, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.debug, \
self.server.projectVersion)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
# decision to unfollow in the web interface is confirmed
if authorized and self.path.endswith('/unfollowconfirm'):
originPathStr=self.path.split('/unfollowconfirm')[0]
followerNickname=getNicknameFromActor(originPathStr)
length = int(self.headers['Content-length'])
followConfirmParams=self.rfile.read(length).decode('utf-8')
if '&submitYes=' in followConfirmParams:
followingActor= \
followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
if '&' in followingActor:
followingActor=followingActor.split('&')[0]
followingNickname=getNicknameFromActor(followingActor)
followingDomain,followingPort=getDomainFromActor(followingActor)
if followerNickname==followingNickname and \
followingDomain==self.server.domain and \
followingPort==self.server.port:
if self.server.debug:
print('You cannot unfollow yourself!')
else:
if self.server.debug:
print(followerNickname+' stops following '+followingActor)
followActor= \
self.server.httpPrefix+'://'+ \
self.server.domainFull+ \
'/users/'+followerNickname
statusNumber,published = getStatusNumber()
followId=followActor+'/statuses/'+str(statusNumber)
unfollowJson = {
'@context': 'https://www.w3.org/ns/activitystreams',
'id': followId+'/undo',
'type': 'Undo',
'actor': followActor,
'object': {
'id': followId,
'type': 'Follow',
'actor': followActor,
'object': followingActor
}
}
pathUsersSection=self.path.split('/users/')[1]
self.postToNickname=pathUsersSection.split('/')[0]
self._postToOutboxThread(unfollowJson)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
# decision to unblock in the web interface is confirmed
if authorized and self.path.endswith('/unblockconfirm'):
originPathStr=self.path.split('/unblockconfirm')[0]
blockerNickname=getNicknameFromActor(originPathStr)
if not blockerNickname:
print('WARN: unable to find nickname in '+originPathStr)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
length = int(self.headers['Content-length'])
blockConfirmParams=self.rfile.read(length).decode('utf-8')
if '&submitYes=' in blockConfirmParams:
blockingActor= \
blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
if '&' in blockingActor:
blockingActor=blockingActor.split('&')[0]
blockingNickname=getNicknameFromActor(blockingActor)
if not blockingNickname:
print('WARN: unable to find nickname in '+blockingActor)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
blockingDomain,blockingPort=getDomainFromActor(blockingActor)
blockingDomainFull=blockingDomain
if blockingPort:
if blockingPort!=80 and blockingPort!=443:
if ':' not in blockingDomain:
blockingDomainFull= \
blockingDomain+':'+str(blockingPort)
if blockerNickname==blockingNickname and \
blockingDomain==self.server.domain and \
blockingPort==self.server.port:
if self.server.debug:
print('You cannot unblock yourself!')
else:
if self.server.debug:
print(blockerNickname+' stops blocking '+blockingActor)
removeBlock(self.server.baseDir,blockerNickname,self.server.domain, \
blockingNickname,blockingDomainFull)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
# decision to block in the web interface is confirmed
if authorized and self.path.endswith('/blockconfirm'):
originPathStr=self.path.split('/blockconfirm')[0]
blockerNickname=getNicknameFromActor(originPathStr)
if not blockerNickname:
print('WARN: unable to find nickname in '+originPathStr)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
length = int(self.headers['Content-length'])
blockConfirmParams=self.rfile.read(length).decode('utf-8')
if '&submitYes=' in blockConfirmParams:
blockingActor= \
blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
if '&' in blockingActor:
blockingActor=blockingActor.split('&')[0]
blockingNickname=getNicknameFromActor(blockingActor)
if not blockingNickname:
print('WARN: unable to find nickname in '+blockingActor)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
blockingDomain,blockingPort= \
getDomainFromActor(blockingActor)
blockingDomainFull=blockingDomain
if blockingPort:
if blockingPort!=80 and blockingPort!=443:
if ':' not in blockingDomain:
blockingDomainFull= \
blockingDomain+':'+str(blockingPort)
if blockerNickname==blockingNickname and \
blockingDomain==self.server.domain and \
blockingPort==self.server.port:
if self.server.debug:
print('You cannot block yourself!')
else:
if self.server.debug:
print('Adding block by '+blockerNickname+ \
' of '+blockingActor)
addBlock(self.server.baseDir,blockerNickname, \
self.server.domain, \
blockingNickname,blockingDomainFull)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
# an option was chosen from person options screen
# view/follow/block/report
if authorized and self.path.endswith('/personoptions'):
pageNumber=1
originPathStr=self.path.split('/personoptions')[0]
chooserNickname=getNicknameFromActor(originPathStr)
if not chooserNickname:
print('WARN: unable to find nickname in '+originPathStr)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
length = int(self.headers['Content-length'])
optionsConfirmParams= \
self.rfile.read(length).decode('utf-8').replace('%3A',':').replace('%2F','/')
# page number to return to
if 'pageNumber=' in optionsConfirmParams:
pageNumberStr=optionsConfirmParams.split('pageNumber=')[1]
if '&' in pageNumberStr:
pageNumberStr=pageNumberStr.split('&')[0]
if pageNumberStr.isdigit():
pageNumber=int(pageNumberStr)
# actor for the person
optionsActor=optionsConfirmParams.split('actor=')[1]
if '&' in optionsActor:
optionsActor=optionsActor.split('&')[0]
# url of the avatar
optionsAvatarUrl=optionsConfirmParams.split('avatarUrl=')[1]
if '&' in optionsAvatarUrl:
optionsAvatarUrl=optionsAvatarUrl.split('&')[0]
# link to a post, which can then be included in reports
postUrl=None
if 'postUrl' in optionsConfirmParams:
postUrl=optionsConfirmParams.split('postUrl=')[1]
if '&' in postUrl:
postUrl=postUrl.split('&')[0]
optionsNickname=getNicknameFromActor(optionsActor)
if not optionsNickname:
print('WARN: unable to find nickname in '+optionsActor)
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
optionsDomain,optionsPort=getDomainFromActor(optionsActor)
optionsDomainFull=optionsDomain
if optionsPort:
if optionsPort!=80 and optionsPort!=443:
if ':' not in optionsDomain:
optionsDomainFull=optionsDomain+':'+str(optionsPort)
if chooserNickname==optionsNickname and \
optionsDomain==self.server.domain and \
optionsPort==self.server.port:
if self.server.debug:
print('You cannot perform an option action on yourself')
if '&submitView=' in optionsConfirmParams:
if self.server.debug:
print('Viewing '+optionsActor)
self._redirect_headers(optionsActor,cookie)
self.server.POSTbusy=False
return
if '&submitBlock=' in optionsConfirmParams:
if self.server.debug:
print('Adding block by '+chooserNickname+ \
' of '+optionsActor)
addBlock(self.server.baseDir,chooserNickname, \
self.server.domain, \
optionsNickname,optionsDomainFull)
if '&submitUnblock=' in optionsConfirmParams:
if self.server.debug:
print('Unblocking '+optionsActor)
msg=htmlUnblockConfirm(self.server.translate, \
self.server.baseDir, \
originPathStr, \
optionsActor, \
optionsAvatarUrl).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.POSTbusy=False
return
if '&submitFollow=' in optionsConfirmParams:
if self.server.debug:
print('Following '+optionsActor)
msg=htmlFollowConfirm(self.server.translate, \
self.server.baseDir, \
originPathStr, \
optionsActor, \
optionsAvatarUrl).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.POSTbusy=False
return
if '&submitUnfollow=' in optionsConfirmParams:
if self.server.debug:
print('Unfollowing '+optionsActor)
msg=htmlUnfollowConfirm(self.server.translate, \
self.server.baseDir, \
originPathStr, \
optionsActor, \
optionsAvatarUrl).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.POSTbusy=False
return
if '&submitDM=' in optionsConfirmParams:
if self.server.debug:
print('Sending DM to '+optionsActor)
reportPath=self.path.replace('/personoptions','')+'/newdm'
msg=htmlNewPost(self.server.translate, \
self.server.baseDir, \
reportPath,None, \
[optionsActor],None, \
pageNumber).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.POSTbusy=False
return
if '&submitSnooze=' in optionsConfirmParams:
thisActor=self.path.split('/personoptions')[0]
if self.server.debug:
print('Snoozing '+optionsActor+' '+thisActor)
if '/users/' in thisActor:
nickname=thisActor.split('/users/')[1]
personSnooze(self.server.baseDir,nickname,self.server.domain,optionsActor)
self._redirect_headers(thisActor+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
if '&submitUnSnooze=' in optionsConfirmParams:
thisActor=self.path.split('/personoptions')[0]
if self.server.debug:
print('Unsnoozing '+optionsActor+' '+thisActor)
if '/users/' in thisActor:
nickname=thisActor.split('/users/')[1]
personUnsnooze(self.server.baseDir,nickname,self.server.domain,optionsActor)
self._redirect_headers(thisActor+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
if '&submitReport=' in optionsConfirmParams:
if self.server.debug:
print('Reporting '+optionsActor)
reportPath=self.path.replace('/personoptions','')+'/newreport'
msg=htmlNewPost(self.server.translate, \
self.server.baseDir, \
reportPath,None,[], \
postUrl,pageNumber).encode()
self._set_headers('text/html',len(msg),cookie)
self._write(msg)
self.server.POSTbusy=False
return
self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False
return
pageNumber=self._receiveNewPost(authorized,'newpost',self.path)
if pageNumber:
nickname=self.path.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
self._redirect_headers('/users/'+nickname+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
pageNumber=self._receiveNewPost(authorized,'newunlisted',self.path)
if pageNumber:
nickname=self.path.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
self._redirect_headers('/users/'+nickname+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
pageNumber=self._receiveNewPost(authorized,'newfollowers',self.path)
if pageNumber:
nickname=self.path.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
self._redirect_headers('/users/'+nickname+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
pageNumber=self._receiveNewPost(authorized,'newdm',self.path)
if pageNumber:
nickname=self.path.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
self._redirect_headers('/users/'+nickname+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
pageNumber=self._receiveNewPost(authorized,'newreport',self.path)
if pageNumber:
nickname=self.path.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
self._redirect_headers('/users/'+nickname+ \
'/inbox?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
pageNumber=self._receiveNewPost(authorized,'newshare',self.path)
if pageNumber:
nickname=self.path.split('/users/')[1]
if '/' in nickname:
nickname=nickname.split('/')[0]
self._redirect_headers('/users/'+nickname+ \
'/shares?page='+str(pageNumber),cookie)
self.server.POSTbusy=False
return
if self.path.endswith('/outbox') or self.path.endswith('/shares'):
if '/users/' in self.path:
if authorized:
self.outboxAuthenticated=True
pathUsersSection=self.path.split('/users/')[1]
self.postToNickname=pathUsersSection.split('/')[0]
if not self.outboxAuthenticated:
self.send_response(405)
self.end_headers()
self.server.POSTbusy=False
return
# check that the post is to an expected path
if not (self.path.endswith('/outbox') or \
self.path.endswith('/inbox') or \
self.path.endswith('/shares') or \
self.path.endswith('/moderationaction') or \
self.path.endswith('/caps/new') or \
self.path=='/sharedInbox'):
print('Attempt to POST to invalid path '+self.path)
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
# read the message and convert it into a python dictionary
length = int(self.headers['Content-length'])
if self.server.debug:
print('DEBUG: content-length: '+str(length))
if not self.headers['Content-type'].startswith('image/') and \
not self.headers['Content-type'].startswith('video/') and \
not self.headers['Content-type'].startswith('audio/'):
if length>self.server.maxMessageLength:
print('Maximum message length exceeded '+str(length))
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
else:
if length>self.server.maxMediaSize:
print('Maximum media size exceeded '+str(length))
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
# receive images to the outbox
if self.headers['Content-type'].startswith('image/') and \
'/users/' in self.path:
if not self.outboxAuthenticated:
if self.server.debug:
print('DEBUG: unauthenticated attempt to post image to outbox')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
pathUsersSection=self.path.split('/users/')[1]
if '/' not in pathUsersSection:
self.send_response(404)
self.end_headers()
self.server.POSTbusy=False
return
self.postFromNickname=pathUsersSection.split('/')[0]
accountsDir= \
self.server.baseDir+'/accounts/'+ \
self.postFromNickname+'@'+self.server.domain
if not os.path.isdir(accountsDir):
self.send_response(404)
self.end_headers()
self.server.POSTbusy=False
return
mediaBytes=self.rfile.read(length)
mediaFilenameBase=accountsDir+'/upload'
mediaFilename=mediaFilenameBase+'.png'
if self.headers['Content-type'].endswith('jpeg'):
mediaFilename=mediaFilenameBase+'.jpg'
if self.headers['Content-type'].endswith('gif'):
mediaFilename=mediaFilenameBase+'.gif'
with open(mediaFilename, 'wb') as avFile:
avFile.write(mediaBytes)
if self.server.debug:
print('DEBUG: image saved to '+mediaFilename)
self.send_response(201)
self.end_headers()
self.server.POSTbusy=False
return
# refuse to receive non-json content
if self.headers['Content-type'] != 'application/json' and \
self.headers['Content-type'] != 'application/activity+json':
print("POST is not json: "+self.headers['Content-type'])
if self.server.debug:
print(str(self.headers))
length = int(self.headers['Content-length'])
if length<self.server.maxPostLength:
unknownPost=self.rfile.read(length).decode('utf-8')
print(str(unknownPost))
self.send_response(400)
self.end_headers()
self.server.POSTbusy=False
return
if self.server.debug:
print('DEBUG: Reading message')
messageBytes=self.rfile.read(length)
messageJson=json.loads(messageBytes)
# https://www.w3.org/TR/activitypub/#object-without-create
if self.outboxAuthenticated:
if self._postToOutbox(messageJson,__version__):
if messageJson.get('id'):
self.headers['Location']= \
messageJson['id'].replace('/activity','').replace('/undo','')
self.send_response(201)
self.end_headers()
self.server.POSTbusy=False
return
else:
if self.server.debug:
print('Failed to post to outbox')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
# check the necessary properties are available
if self.server.debug:
print('DEBUG: Check message has params')
if self.path.endswith('/inbox') or \
self.path=='/sharedInbox':
if not inboxMessageHasParams(messageJson):
if self.server.debug:
pprint(messageJson)
print("DEBUG: inbox message doesn't have the required parameters")
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
if not inboxPermittedMessage(self.server.domain, \
messageJson, \
self.server.federationList):
if self.server.debug:
# https://www.youtube.com/watch?v=K3PrSj9XEu4
print('DEBUG: Ah Ah Ah')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
if self.server.debug:
pprint(messageJson)
if not self.headers.get('signature'):
if 'keyId=' not in self.headers['signature']:
if self.server.debug:
print('DEBUG: POST to inbox has no keyId in header signature parameter')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
if self.server.debug:
print('DEBUG: POST saving to inbox queue')
if '/users/' in self.path:
pathUsersSection=self.path.split('/users/')[1]
if '/' not in pathUsersSection:
if self.server.debug:
print('DEBUG: This is not a users endpoint')
else:
self.postToNickname=pathUsersSection.split('/')[0]
if self.postToNickname:
queueStatus= \
self._updateInboxQueue(self.postToNickname, \
messageJson,messageBytes)
if queueStatus==0:
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
return
if queueStatus==1:
self.send_response(503)
self.end_headers()
self.server.POSTbusy=False
return
if self.server.debug:
print('_updateInboxQueue exited without doing anything')
else:
if self.server.debug:
print('self.postToNickname is None')
self.send_response(403)
self.end_headers()
self.server.POSTbusy=False
return
else:
if self.path == '/sharedInbox' or self.path == '/inbox':
print('DEBUG: POST to shared inbox')
queueStatus= \
self._updateInboxQueue('inbox',messageJson,messageBytes)
if queueStatus==0:
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
return
if queueStatus==1:
self.send_response(503)
self.end_headers()
self.server.POSTbusy=False
return
self.send_response(200)
self.end_headers()
self.server.POSTbusy=False
class PubServerUnitTest(PubServer):
protocol_version = 'HTTP/1.0'
def runPostsQueue(baseDir: str,sendThreads: [],debug: bool) -> None:
"""Manages the threads used to send posts
"""
while True:
time.sleep(1)
removeDormantThreads(baseDir,sendThreads,debug)
def runSharesExpire(versionNumber: str,baseDir: str) -> None:
"""Expires shares as needed
"""
while True:
time.sleep(120)
expireShares(baseDir)
def runPostsWatchdog(projectVersion: str,httpd) -> None:
"""This tries to keep the posts thread running even if it dies
"""
print('Starting posts queue watchdog')
postsQueueOriginal=httpd.thrPostsQueue.clone(runPostsQueue)
httpd.thrPostsQueue.start()
while True:
time.sleep(20)
if not httpd.thrPostsQueue.isAlive():
httpd.thrPostsQueue.kill()
httpd.thrPostsQueue=postsQueueOriginal.clone(runPostsQueue)
httpd.thrPostsQueue.start()
print('Restarting posts queue...')
def runSharesExpireWatchdog(projectVersion: str,httpd) -> None:
"""This tries to keep the shares expiry thread running even if it dies
"""
print('Starting shares expiry watchdog')
sharesExpireOriginal=httpd.thrSharesExpire.clone(runSharesExpire)
httpd.thrSharesExpire.start()
while True:
time.sleep(20)
if not httpd.thrSharesExpire.isAlive():
httpd.thrSharesExpire.kill()
httpd.thrSharesExpire=sharesExpireOriginal.clone(runSharesExpire)
httpd.thrSharesExpire.start()
print('Restarting shares expiry...')
def loadTokens(baseDir: str,tokensDict: {},tokensLookup: {}) -> None:
for subdir, dirs, files in os.walk(baseDir+'/accounts'):
for handle in dirs:
if '@' in handle:
tokenFilename=baseDir+'/accounts/'+handle+'/.token'
if not os.path.isfile(tokenFilename):
continue
nickname=handle.split('@')[0]
token=None
try:
with open(tokenFilename, 'r') as fp:
token = fp.read()
except Exception as e:
print('WARN: Unable to read token for '+nickname+' '+str(e))
if not token:
continue
tokensDict[nickname]=token
tokensLookup[token]=nickname
def runDaemon(projectVersion, \
instanceId,clientToServer: bool, \
baseDir: str,domain: str, \
port=80,proxyPort=80,httpPrefix='https', \
fedList=[],maxMentions=10, \
authenticatedFetch=False, \
noreply=False,nolike=False,nopics=False, \
noannounce=False,cw=False,ocapAlways=False, \
useTor=False,maxReplies=64, \
domainMaxPostsPerDay=8640,accountMaxPostsPerDay=8640, \
allowDeletion=False,debug=False,unitTest=False, \
instanceOnlySkillsSearch=False,sendThreads=[]) -> None:
if len(domain)==0:
domain='localhost'
if '.' not in domain:
if domain != 'localhost':
print('Invalid domain: ' + domain)
return
serverAddress = ('', proxyPort)
if unitTest:
httpd = ThreadingHTTPServer(serverAddress, PubServerUnitTest)
else:
httpd = ThreadingHTTPServer(serverAddress, PubServer)
# load translations dictionary
httpd.translate={}
if not unitTest:
if not os.path.isdir(baseDir+'/translations'):
print('ERROR: translations directory not found')
return
systemLanguage=locale.getdefaultlocale()[0]
if '_' in systemLanguage:
systemLanguage=systemLanguage.split('_')[0]
if '.' in systemLanguage:
systemLanguage=systemLanguage.split('.')[0]
translationsFile=baseDir+'/translations/'+systemLanguage+'.json'
if not os.path.isfile(translationsFile):
systemLanguage='en'
translationsFile=baseDir+'/translations/'+systemLanguage+'.json'
print('System language: '+systemLanguage)
httpd.translate=loadJson(translationsFile)
httpd.outboxThread={}
httpd.newPostThread={}
httpd.projectVersion=projectVersion
httpd.authenticatedFetch=authenticatedFetch
# max POST size of 30M
httpd.maxPostLength=1024*1024*30
httpd.maxMediaSize=httpd.maxPostLength
httpd.maxMessageLength=5000
httpd.maxPostsInBox=32000
httpd.domain=domain
httpd.port=port
httpd.domainFull=domain
if port:
if port!=80 and port!=443:
if ':' not in domain:
httpd.domainFull=domain+':'+str(port)
httpd.httpPrefix=httpPrefix
httpd.debug=debug
httpd.federationList=fedList.copy()
httpd.baseDir=baseDir
httpd.instanceId=instanceId
httpd.personCache={}
httpd.cachedWebfingers={}
httpd.useTor=useTor
httpd.session = None
httpd.sessionLastUpdate=0
httpd.lastGET=0
httpd.lastPOST=0
httpd.GETbusy=False
httpd.POSTbusy=False
httpd.receivedMessage=False
httpd.inboxQueue=[]
httpd.sendThreads=sendThreads
httpd.postLog=[]
httpd.maxQueueLength=16
httpd.ocapAlways=ocapAlways
httpd.allowDeletion=allowDeletion
httpd.lastLoginTime=0
httpd.maxReplies=maxReplies
httpd.tokens={}
httpd.tokensLookup={}
loadTokens(baseDir,httpd.tokens,httpd.tokensLookup)
httpd.instanceOnlySkillsSearch=instanceOnlySkillsSearch
httpd.acceptedCaps=["inbox:write","objects:read"]
# contains threads used to send posts to followers
httpd.followersThreads=[]
if noreply:
httpd.acceptedCaps.append('inbox:noreply')
if nolike:
httpd.acceptedCaps.append('inbox:nolike')
if nopics:
httpd.acceptedCaps.append('inbox:nopics')
if noannounce:
httpd.acceptedCaps.append('inbox:noannounce')
if cw:
httpd.acceptedCaps.append('inbox:cw')
if not os.path.isdir(baseDir+'/accounts/inbox@'+domain):
print('Creating shared inbox: inbox@'+domain)
createSharedInbox(baseDir,'inbox',domain,port,httpPrefix)
if not os.path.isdir(baseDir+'/cache'):
os.mkdir(baseDir+'/cache')
if not os.path.isdir(baseDir+'/cache/actors'):
print('Creating actors cache')
os.mkdir(baseDir+'/cache/actors')
if not os.path.isdir(baseDir+'/cache/announce'):
print('Creating announce cache')
os.mkdir(baseDir+'/cache/announce')
if not os.path.isdir(baseDir+'/cache/avatars'):
print('Creating avatars cache')
os.mkdir(baseDir+'/cache/avatars')
archiveDir=baseDir+'/archive'
if not os.path.isdir(archiveDir):
print('Creating archive')
os.mkdir(archiveDir)
print('Creating cache expiry thread')
httpd.thrCache= \
threadWithTrace(target=expireCache, \
args=(baseDir,httpd.personCache, \
httpd.httpPrefix, \
archiveDir, \
httpd.maxPostsInBox),daemon=True)
httpd.thrCache.start()
print('Creating posts queue')
httpd.thrPostsQueue= \
threadWithTrace(target=runPostsQueue, \
args=(baseDir,httpd.sendThreads,debug),daemon=True)
if not unitTest:
httpd.thrPostsWatchdog= \
threadWithTrace(target=runPostsWatchdog, \
args=(projectVersion,httpd),daemon=True)
httpd.thrPostsWatchdog.start()
else:
httpd.thrPostsQueue.start()
print('Creating expire thread for shared items')
httpd.thrSharesExpire= \
threadWithTrace(target=runSharesExpire, \
args=(__version__,baseDir),daemon=True)
if not unitTest:
httpd.thrSharesExpireWatchdog= \
threadWithTrace(target=runSharesExpireWatchdog, \
args=(projectVersion,httpd),daemon=True)
httpd.thrSharesExpireWatchdog.start()
else:
httpd.thrSharesExpire.start()
print('Creating inbox queue')
httpd.thrInboxQueue= \
threadWithTrace(target=runInboxQueue, \
args=(projectVersion, \
baseDir,httpPrefix,httpd.sendThreads, \
httpd.postLog,httpd.cachedWebfingers, \
httpd.personCache,httpd.inboxQueue, \
domain,port,useTor,httpd.federationList, \
httpd.ocapAlways,maxReplies, \
domainMaxPostsPerDay,accountMaxPostsPerDay, \
allowDeletion,debug,maxMentions,httpd.translate, \
unitTest,httpd.acceptedCaps),daemon=True)
if not unitTest:
httpd.thrWatchdog= \
threadWithTrace(target=runInboxQueueWatchdog, \
args=(projectVersion,httpd),daemon=True)
httpd.thrWatchdog.start()
else:
httpd.thrInboxQueue.start()
if clientToServer:
print('Running ActivityPub client on ' + domain + ' port ' + str(proxyPort))
else:
print('Running ActivityPub server on ' + domain + ' port ' + str(proxyPort))
httpd.serve_forever()