daemon.py 193 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007300830093010301130123013301430153016301730183019302030213022302330243025302630273028302930303031303230333034303530363037303830393040304130423043304430453046304730483049305030513052305330543055305630573058305930603061306230633064306530663067306830693070307130723073307430753076307730783079308030813082308330843085308630873088308930903091309230933094309530963097309830993100310131023103310431053106310731083109311031113112311331143115311631173118311931203121312231233124312531263127312831293130313131323133313431353136313731383139314031413142314331443145314631473148314931503151315231533154315531563157315831593160316131623163316431653166316731683169317031713172317331743175317631773178317931803181318231833184318531863187318831893190319131923193319431953196319731983199320032013202320332043205320632073208320932103211321232133214321532163217321832193220322132223223322432253226322732283229323032313232323332343235323632373238323932403241324232433244324532463247324832493250325132523253325432553256325732583259326032613262326332643265326632673268326932703271327232733274327532763277327832793280328132823283328432853286328732883289329032913292329332943295329632973298329933003301330233033304330533063307330833093310331133123313331433153316331733183319332033213322332333243325332633273328332933303331333233333334333533363337333833393340334133423343334433453346334733483349335033513352335333543355335633573358335933603361336233633364336533663367336833693370337133723373337433753376337733783379338033813382338333843385338633873388338933903391339233933394339533963397339833993400340134023403340434053406340734083409341034113412341334143415341634173418341934203421342234233424342534263427342834293430343134323433343434353436343734383439344034413442344334443445344634473448344934503451345234533454345534563457345834593460346134623463346434653466346734683469347034713472347334743475347634773478347934803481348234833484348534863487348834893490349134923493349434953496349734983499350035013502350335043505350635073508350935103511351235133514351535163517351835193520352135223523352435253526352735283529353035313532353335343535353635373538353935403541354235433544354535463547354835493550355135523553355435553556355735583559356035613562356335643565356635673568356935703571357235733574357535763577357835793580358135823583358435853586358735883589359035913592359335943595359635973598359936003601360236033604360536063607360836093610361136123613361436153616361736183619362036213622362336243625362636273628362936303631363236333634363536363637363836393640364136423643364436453646364736483649365036513652365336543655365636573658365936603661366236633664366536663667366836693670367136723673367436753676367736783679368036813682368336843685368636873688368936903691369236933694369536963697369836993700370137023703370437053706370737083709371037113712371337143715371637173718371937203721372237233724372537263727372837293730373137323733373437353736373737383739374037413742374337443745374637473748374937503751375237533754375537563757375837593760376137623763376437653766376737683769377037713772377337743775377637773778377937803781378237833784378537863787378837893790379137923793379437953796379737983799380038013802380338043805380638073808380938103811381238133814381538163817381838193820382138223823382438253826382738283829383038313832383338343835383638373838383938403841384238433844384538463847384838493850385138523853385438553856385738583859386038613862386338643865386638673868386938703871387238733874387538763877387838793880388138823883388438853886388738883889
  1. __filename__ = "daemon.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.0.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer
  9. #import socketserver
  10. import commentjson
  11. import json
  12. import time
  13. import base64
  14. import locale
  15. # used for mime decoding of message POST
  16. import email.parser
  17. # for saving images
  18. from binascii import a2b_base64
  19. from hashlib import sha256
  20. from pprint import pprint
  21. from session import createSession
  22. from webfinger import webfingerMeta
  23. from webfinger import webfingerLookup
  24. from webfinger import webfingerHandle
  25. from person import registerAccount
  26. from person import personLookup
  27. from person import personBoxJson
  28. from person import createSharedInbox
  29. from person import isSuspended
  30. from person import suspendAccount
  31. from person import unsuspendAccount
  32. from person import removeAccount
  33. from person import canRemovePost
  34. from posts import outboxMessageCreateWrap
  35. from posts import savePostToBox
  36. from posts import sendToFollowers
  37. from posts import postIsAddressedToPublic
  38. from posts import sendToNamedAddresses
  39. from posts import createPublicPost
  40. from posts import createReportPost
  41. from posts import createUnlistedPost
  42. from posts import createFollowersOnlyPost
  43. from posts import createDirectMessagePost
  44. from posts import populateRepliesJson
  45. from posts import addToField
  46. from posts import expireCache
  47. from inbox import inboxPermittedMessage
  48. from inbox import inboxMessageHasParams
  49. from inbox import runInboxQueue
  50. from inbox import runInboxQueueWatchdog
  51. from inbox import savePostToInboxQueue
  52. from inbox import populateReplies
  53. from follow import getFollowingFeed
  54. from follow import outboxUndoFollow
  55. from follow import sendFollowRequest
  56. from auth import authorize
  57. from auth import createPassword
  58. from auth import createBasicAuthHeader
  59. from auth import authorizeBasic
  60. from threads import threadWithTrace
  61. from media import getMediaPath
  62. from media import createMediaDirs
  63. from delete import outboxDelete
  64. from like import outboxLike
  65. from like import outboxUndoLike
  66. from blocking import outboxBlock
  67. from blocking import outboxUndoBlock
  68. from blocking import addBlock
  69. from blocking import removeBlock
  70. from blocking import addGlobalBlock
  71. from blocking import removeGlobalBlock
  72. from blocking import isBlockedHashtag
  73. from blocking import isBlockedDomain
  74. from config import setConfigParam
  75. from config import getConfigParam
  76. from roles import outboxDelegate
  77. from roles import setRole
  78. from roles import clearModeratorStatus
  79. from skills import outboxSkills
  80. from availability import outboxAvailability
  81. from webinterface import htmlDeletePost
  82. from webinterface import htmlAbout
  83. from webinterface import htmlRemoveSharedItem
  84. from webinterface import htmlInboxDMs
  85. from webinterface import htmlUnblockConfirm
  86. from webinterface import htmlPersonOptions
  87. from webinterface import htmlIndividualPost
  88. from webinterface import htmlProfile
  89. from webinterface import htmlInbox
  90. from webinterface import htmlOutbox
  91. from webinterface import htmlModeration
  92. from webinterface import htmlPostReplies
  93. from webinterface import htmlLogin
  94. from webinterface import htmlSuspended
  95. from webinterface import htmlGetLoginCredentials
  96. from webinterface import htmlNewPost
  97. from webinterface import htmlFollowConfirm
  98. from webinterface import htmlSearch
  99. from webinterface import htmlSearchEmoji
  100. from webinterface import htmlSearchEmojiTextEntry
  101. from webinterface import htmlUnfollowConfirm
  102. from webinterface import htmlProfileAfterSearch
  103. from webinterface import htmlEditProfile
  104. from webinterface import htmlTermsOfService
  105. from webinterface import htmlSkillsSearch
  106. from webinterface import htmlHashtagSearch
  107. from webinterface import htmlModerationInfo
  108. from webinterface import htmlSearchSharedItems
  109. from webinterface import htmlHashtagBlocked
  110. from shares import getSharesFeedForPerson
  111. from shares import outboxShareUpload
  112. from shares import outboxUndoShareUpload
  113. from shares import addShare
  114. from shares import removeShare
  115. from utils import getNicknameFromActor
  116. from utils import getDomainFromActor
  117. from utils import getStatusNumber
  118. from manualapprove import manualDenyFollowRequest
  119. from manualapprove import manualApproveFollowRequest
  120. from announce import createAnnounce
  121. from announce import outboxAnnounce
  122. from content import addHtmlTags
  123. from media import removeMetaData
  124. from cache import storePersonInCache
  125. import os
  126. import sys
  127. # maximum number of posts to list in outbox feed
  128. maxPostsInFeed=12
  129. # number of follows/followers per page
  130. followsPerPage=12
  131. # number of item shares per page
  132. sharesPerPage=12
  133. def readFollowList(filename: str) -> []:
  134. """Returns a list of ActivityPub addresses to follow
  135. """
  136. followlist=[]
  137. if not os.path.isfile(filename):
  138. return followlist
  139. followUsers = open(filename, "r")
  140. for u in followUsers:
  141. if u not in followlist:
  142. nickname,domain = parseHandle(u)
  143. if nickname:
  144. followlist.append(nickname+'@'+domain)
  145. followUsers.close()
  146. return followlist
  147. class PubServer(BaseHTTPRequestHandler):
  148. protocol_version = 'HTTP/1.1'
  149. def _requestHTTP(self) -> bool:
  150. """Should a http response be given?
  151. """
  152. if not self.headers.get('Accept'):
  153. return False
  154. if 'image/' in self.headers['Accept']:
  155. return False
  156. if self.headers['Accept'].startswith('*'):
  157. return False
  158. if 'json' in self.headers['Accept']:
  159. return False
  160. return True
  161. def _login_headers(self,fileFormat: str,length: int) -> None:
  162. self.send_response(200)
  163. self.send_header('Content-type', fileFormat)
  164. self.send_header('Content-Length', str(length))
  165. self.send_header('Host', self.server.domainFull)
  166. self.send_header('WWW-Authenticate', \
  167. 'title="Login to Epicyon", Basic realm="epicyon"')
  168. self.send_header('X-Robots-Tag','noindex')
  169. self.end_headers()
  170. def _set_headers(self,fileFormat: str,length: int,cookie: str) -> None:
  171. self.send_response(200)
  172. self.send_header('Content-type', fileFormat)
  173. self.send_header('Content-Length', str(length))
  174. if cookie:
  175. self.send_header('Cookie', cookie)
  176. self.send_header('Host', self.server.domainFull)
  177. self.send_header('InstanceID', self.server.instanceId)
  178. self.send_header('X-Robots-Tag','noindex')
  179. self.end_headers()
  180. def _redirect_headers(self,redirect: str,cookie: str) -> None:
  181. self.send_response(303)
  182. #self.send_header('Content-type', 'text/html')
  183. if cookie:
  184. self.send_header('Cookie', cookie)
  185. self.send_header('Location', redirect)
  186. self.send_header('Host', self.server.domainFull)
  187. self.send_header('InstanceID', self.server.instanceId)
  188. self.send_header('Content-Length', '0')
  189. self.send_header('X-Robots-Tag','noindex')
  190. self.end_headers()
  191. def _404(self) -> None:
  192. msg="<html><head></head><body><h1>404 Not Found</h1></body></html>".encode('utf-8')
  193. self.send_response(404)
  194. self.send_header('Content-Type', 'text/html; charset=utf-8')
  195. self.send_header('Content-Length', str(len(msg)))
  196. self.send_header('X-Robots-Tag','noindex')
  197. self.end_headers()
  198. try:
  199. self.wfile.write(msg)
  200. except Exception as e:
  201. print('Error when showing 404')
  202. print(e)
  203. def _robotsTxt(self) -> bool:
  204. if not self.path.lower().startswith('/robot'):
  205. return False
  206. msg='User-agent: *\nDisallow: /'
  207. msg=msg.encode('utf-8')
  208. self._set_headers('text/plain; charset=utf-8',len(msg),None)
  209. self.wfile.write(msg)
  210. return True
  211. def _webfinger(self) -> bool:
  212. if not self.path.startswith('/.well-known'):
  213. return False
  214. if self.server.debug:
  215. print('DEBUG: WEBFINGER well-known')
  216. if self.server.debug:
  217. print('DEBUG: WEBFINGER host-meta')
  218. if self.path.startswith('/.well-known/host-meta'):
  219. wfResult= \
  220. webfingerMeta(self.server.httpPrefix,self.server.domainFull)
  221. if wfResult:
  222. msg=wfResult.encode('utf-8')
  223. self._set_headers('application/xrd+xml',len(msg),None)
  224. self.wfile.write(msg)
  225. return
  226. if self.server.debug:
  227. print('DEBUG: WEBFINGER lookup '+self.path+' '+ \
  228. str(self.server.baseDir))
  229. wfResult= \
  230. webfingerLookup(self.path,self.server.baseDir, \
  231. self.server.port,self.server.debug)
  232. if wfResult:
  233. msg=json.dumps(wfResult).encode('utf-8')
  234. self._set_headers('application/jrd+json',len(msg),None)
  235. self.wfile.write(msg)
  236. else:
  237. if self.server.debug:
  238. print('DEBUG: WEBFINGER lookup 404 '+self.path)
  239. self._404()
  240. return True
  241. def _permittedDir(self,path: str) -> bool:
  242. """These are special paths which should not be accessible
  243. directly via GET or POST
  244. """
  245. if path.startswith('/wfendpoints') or \
  246. path.startswith('/keys') or \
  247. path.startswith('/accounts'):
  248. return False
  249. return True
  250. def _postToOutbox(self,messageJson: {},version: str) -> bool:
  251. """post is received by the outbox
  252. Client to server message post
  253. https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
  254. """
  255. if not messageJson.get('type'):
  256. if self.server.debug:
  257. print('DEBUG: POST to outbox has no "type" parameter')
  258. return False
  259. if not messageJson.get('object') and messageJson.get('content'):
  260. if messageJson['type']!='Create':
  261. # https://www.w3.org/TR/activitypub/#object-without-create
  262. if self.server.debug:
  263. print('DEBUG: POST to outbox - adding Create wrapper')
  264. messageJson= \
  265. outboxMessageCreateWrap(self.server.httpPrefix, \
  266. self.postToNickname, \
  267. self.server.domain, \
  268. self.server.port, \
  269. messageJson)
  270. if messageJson['type']=='Create':
  271. if not (messageJson.get('id') and \
  272. messageJson.get('type') and \
  273. messageJson.get('actor') and \
  274. messageJson.get('object') and \
  275. messageJson.get('to')):
  276. if self.server.debug:
  277. pprint(messageJson)
  278. print('DEBUG: POST to outbox - Create does not have the required parameters')
  279. return False
  280. testDomain,testPort=getDomainFromActor(messageJson['actor'])
  281. if testPort:
  282. if testPort!=80 and testPort!=443:
  283. testDomain=testDomain+':'+str(testPort)
  284. if isBlockedDomain(self.server.baseDir,testDomain):
  285. if self.server.debug:
  286. print('DEBUG: domain is blocked: '+messageJson['actor'])
  287. return False
  288. # https://www.w3.org/TR/activitypub/#create-activity-outbox
  289. messageJson['object']['attributedTo']=messageJson['actor']
  290. if messageJson['object'].get('attachment'):
  291. attachmentIndex=0
  292. if messageJson['object']['attachment'][attachmentIndex].get('mediaType'):
  293. fileExtension='png'
  294. mediaTypeStr= \
  295. messageJson['object']['attachment'][attachmentIndex]['mediaType']
  296. if mediaTypeStr.endswith('jpeg'):
  297. fileExtension='jpg'
  298. elif mediaTypeStr.endswith('gif'):
  299. fileExtension='gif'
  300. elif mediaTypeStr.endswith('audio/mpeg'):
  301. fileExtension='mp3'
  302. elif mediaTypeStr.endswith('ogg'):
  303. fileExtension='ogg'
  304. elif mediaTypeStr.endswith('mp4'):
  305. fileExtension='mp4'
  306. elif mediaTypeStr.endswith('webm'):
  307. fileExtension='webm'
  308. elif mediaTypeStr.endswith('ogv'):
  309. fileExtension='ogv'
  310. mediaDir= \
  311. self.server.baseDir+'/accounts/'+ \
  312. self.postToNickname+'@'+self.server.domain
  313. uploadMediaFilename=mediaDir+'/upload.'+fileExtension
  314. if not os.path.isfile(uploadMediaFilename):
  315. del messageJson['object']['attachment']
  316. else:
  317. # generate a path for the uploaded image
  318. mPath=getMediaPath()
  319. mediaPath= \
  320. mPath+'/'+createPassword(32)+'.'+fileExtension
  321. createMediaDirs(self.server.baseDir,mPath)
  322. mediaFilename=self.server.baseDir+'/'+mediaPath
  323. # move the uploaded image to its new path
  324. os.rename(uploadMediaFilename,mediaFilename)
  325. # change the url of the attachment
  326. messageJson['object']['attachment'][attachmentIndex]['url']= \
  327. self.server.httpPrefix+'://'+ \
  328. self.server.domainFull+'/'+mediaPath
  329. permittedOutboxTypes=[
  330. 'Create','Announce','Like','Follow','Undo', \
  331. 'Update','Add','Remove','Block','Delete', \
  332. 'Delegate','Skill'
  333. ]
  334. if messageJson['type'] not in permittedOutboxTypes:
  335. if self.server.debug:
  336. print('DEBUG: POST to outbox - '+messageJson['type']+ \
  337. ' is not a permitted activity type')
  338. return False
  339. if messageJson.get('id'):
  340. postId=messageJson['id'].replace('/activity','').replace('/undo','')
  341. if self.server.debug:
  342. print('DEBUG: id attribute exists within POST to outbox')
  343. else:
  344. if self.server.debug:
  345. print('DEBUG: No id attribute within POST to outbox')
  346. postId=None
  347. if self.server.debug:
  348. pprint(messageJson)
  349. print('DEBUG: savePostToBox')
  350. savePostToBox(self.server.baseDir, \
  351. self.server.httpPrefix, \
  352. postId, \
  353. self.postToNickname, \
  354. self.server.domainFull,messageJson,'outbox')
  355. if outboxAnnounce(self.server.baseDir,messageJson,self.server.debug):
  356. if self.server.debug:
  357. print('DEBUG: Updated announcements (shares) collection for the post associated with the Announce activity')
  358. if not self.server.session:
  359. if self.server.debug:
  360. print('DEBUG: creating new session for c2s')
  361. self.server.session= \
  362. createSession(self.server.domain,self.server.port, \
  363. self.server.useTor)
  364. if self.server.debug:
  365. print('DEBUG: sending c2s post to followers')
  366. sendToFollowers(self.server.session,self.server.baseDir, \
  367. self.postToNickname,self.server.domain, \
  368. self.server.port, \
  369. self.server.httpPrefix, \
  370. self.server.federationList, \
  371. self.server.sendThreads, \
  372. self.server.postLog, \
  373. self.server.cachedWebfingers, \
  374. self.server.personCache, \
  375. messageJson,self.server.debug, \
  376. self.server.projectVersion)
  377. if self.server.debug:
  378. print('DEBUG: handle any unfollow requests')
  379. outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
  380. if self.server.debug:
  381. print('DEBUG: handle delegation requests')
  382. outboxDelegate(self.server.baseDir,self.postToNickname,messageJson, \
  383. self.server.debug)
  384. if self.server.debug:
  385. print('DEBUG: handle skills changes requests')
  386. outboxSkills(self.server.baseDir,self.postToNickname,messageJson, \
  387. self.server.debug)
  388. if self.server.debug:
  389. print('DEBUG: handle availability changes requests')
  390. outboxAvailability(self.server.baseDir,self.postToNickname,messageJson, \
  391. self.server.debug)
  392. if self.server.debug:
  393. print('DEBUG: handle any like requests')
  394. outboxLike(self.server.baseDir,self.server.httpPrefix, \
  395. self.postToNickname,self.server.domain,self.server.port, \
  396. messageJson,self.server.debug)
  397. if self.server.debug:
  398. print('DEBUG: handle any undo like requests')
  399. outboxUndoLike(self.server.baseDir,self.server.httpPrefix, \
  400. self.postToNickname,self.server.domain,self.server.port, \
  401. messageJson,self.server.debug)
  402. if self.server.debug:
  403. print('DEBUG: handle delete requests')
  404. outboxDelete(self.server.baseDir,self.server.httpPrefix, \
  405. self.postToNickname,self.server.domain, \
  406. messageJson,self.server.debug, \
  407. self.server.allowDeletion)
  408. if self.server.debug:
  409. print('DEBUG: handle block requests')
  410. outboxBlock(self.server.baseDir,self.server.httpPrefix, \
  411. self.postToNickname,self.server.domain, \
  412. self.server.port,
  413. messageJson,self.server.debug)
  414. if self.server.debug:
  415. print('DEBUG: handle undo block requests')
  416. outboxUndoBlock(self.server.baseDir,self.server.httpPrefix, \
  417. self.postToNickname,self.server.domain, \
  418. self.server.port,
  419. messageJson,self.server.debug)
  420. if self.server.debug:
  421. print('DEBUG: handle share uploads')
  422. outboxShareUpload(self.server.baseDir,self.server.httpPrefix, \
  423. self.postToNickname,self.server.domain, \
  424. self.server.port,
  425. messageJson,self.server.debug)
  426. if self.server.debug:
  427. print('DEBUG: handle undo share uploads')
  428. outboxUndoShareUpload(self.server.baseDir,self.server.httpPrefix, \
  429. self.postToNickname,self.server.domain, \
  430. self.server.port,
  431. messageJson,self.server.debug)
  432. if self.server.debug:
  433. print('DEBUG: sending c2s post to named addresses')
  434. print('c2s sender: '+self.postToNickname+'@'+self.server.domain+ \
  435. ':'+str(self.server.port))
  436. sendToNamedAddresses(self.server.session,self.server.baseDir, \
  437. self.postToNickname,self.server.domain, \
  438. self.server.port, \
  439. self.server.httpPrefix, \
  440. self.server.federationList, \
  441. self.server.sendThreads, \
  442. self.server.postLog, \
  443. self.server.cachedWebfingers, \
  444. self.server.personCache, \
  445. messageJson,self.server.debug, \
  446. self.server.projectVersion)
  447. return True
  448. def _postToOutboxThread(self,messageJson: {}) -> bool:
  449. """Creates a thread to send a post
  450. """
  451. accountOutboxThreadName=self.postToNickname
  452. if not accountOutboxThreadName:
  453. accountOutboxThreadName='*'
  454. if self.server.outboxThread.get(accountOutboxThreadName):
  455. print('Waiting for previous outbox thread to end')
  456. waitCtr=0
  457. while self.server.outboxThread[accountOutboxThreadName].isAlive() and waitCtr<8:
  458. time.sleep(1)
  459. waitCtr+=1
  460. if waitCtr>=8:
  461. self.server.outboxThread[accountOutboxThreadName].kill()
  462. print('Creating outbox thread')
  463. self.server.outboxThread[accountOutboxThreadName]= \
  464. threadWithTrace(target=self._postToOutbox, \
  465. args=(messageJson.copy(),__version__),daemon=True)
  466. print('Starting outbox thread')
  467. self.server.outboxThread[accountOutboxThreadName].start()
  468. return True
  469. def _inboxQueueCleardown(self) -> None:
  470. """ Check if the queue is full and remove oldest items if it is
  471. """
  472. if len(self.server.inboxQueue)>=self.server.maxQueueLength:
  473. print('Inbox queue is full. Removing oldest items.')
  474. while len(self.server.inboxQueue) >= self.server.maxQueueLength-4:
  475. queueFilename=self.server.inboxQueue[0]
  476. if os.path.isfile(queueFilename):
  477. os.remove(queueFilename)
  478. self.server.inboxQueue.pop(0)
  479. def _updateInboxQueue(self,nickname: str,messageJson: {},messageBytes: str) -> int:
  480. """Update the inbox queue
  481. """
  482. self._inboxQueueCleardown()
  483. # Convert the headers needed for signature verification to dict
  484. headersDict={}
  485. headersDict['host']=self.headers['host']
  486. headersDict['signature']=self.headers['signature']
  487. if self.headers.get('Date'):
  488. headersDict['Date']=self.headers['Date']
  489. if self.headers.get('digest'):
  490. headersDict['digest']=self.headers['digest']
  491. if self.headers.get('Content-type'):
  492. headersDict['Content-type']=self.headers['Content-type']
  493. # For follow activities add a 'to' field, which is a copy of
  494. # the object field
  495. messageJson,toFieldExists= \
  496. addToField('Follow',messageJson,self.server.debug)
  497. # For like activities add a 'to' field, which is a copy of
  498. # the actor within the object field
  499. messageJson,toFieldExists= \
  500. addToField('Like',messageJson,self.server.debug)
  501. pprint(messageJson)
  502. # save the json for later queue processing
  503. queueFilename = \
  504. savePostToInboxQueue(self.server.baseDir,
  505. self.server.httpPrefix,
  506. nickname,
  507. self.server.domainFull,
  508. messageJson,
  509. messageBytes.decode('utf-8'),
  510. headersDict,
  511. self.path,
  512. self.server.debug)
  513. if queueFilename:
  514. # add json to the queue
  515. if queueFilename not in self.server.inboxQueue:
  516. self.server.inboxQueue.append(queueFilename)
  517. self.send_response(201)
  518. self.end_headers()
  519. self.server.POSTbusy=False
  520. return 0
  521. return 2
  522. def _isAuthorized(self) -> bool:
  523. # token based authenticated used by the web interface
  524. if self.headers.get('Cookie'):
  525. if '=' in self.headers['Cookie']:
  526. tokenStr=self.headers['Cookie'].split('=',1)[1]
  527. if self.server.tokensLookup.get(tokenStr):
  528. nickname=self.server.tokensLookup[tokenStr]
  529. # check that the path contains the same nickname as
  530. # the cookie otherwise it would be possible to be
  531. # authorized to use an account you don't own
  532. if '/'+nickname+'/' in self.path:
  533. return True
  534. if self.path.endswith('/'+nickname):
  535. return True
  536. return False
  537. # basic auth
  538. if self.headers.get('Authorization'):
  539. if authorize(self.server.baseDir,self.path, \
  540. self.headers['Authorization'], \
  541. self.server.debug):
  542. return True
  543. return False
  544. def do_GET(self):
  545. # redirect music to #nowplaying list
  546. if self.path=='/music' or self.path=='/nowplaying':
  547. self.path='/tags/nowplaying'
  548. if self.server.debug:
  549. print('DEBUG: GET from '+self.server.baseDir+ \
  550. ' path: '+self.path+' busy: '+ \
  551. str(self.server.GETbusy))
  552. if self.server.debug:
  553. print(str(self.headers))
  554. cookie=None
  555. if self.headers.get('Cookie'):
  556. cookie=self.headers['Cookie']
  557. # check authorization
  558. authorized = self._isAuthorized()
  559. if authorized:
  560. if self.server.debug:
  561. print('GET Authorization granted')
  562. else:
  563. if self.server.debug:
  564. print('GET Not authorized')
  565. if not self.server.session:
  566. self.server.session= \
  567. createSession(self.server.domain,self.server.port, \
  568. self.server.useTor)
  569. # is this a html request?
  570. htmlGET=False
  571. if self.headers.get('Accept'):
  572. if self._requestHTTP():
  573. htmlGET=True
  574. # replace https://domain/@nick with https://domain/users/nick
  575. if self.path.startswith('/@'):
  576. self.path=self.path.replace('/@','/users/')
  577. # treat shared inbox paths consistently
  578. if self.path=='/sharedInbox' or \
  579. self.path=='/users/inbox' or \
  580. self.path=='/actor/inbox' or \
  581. self.path=='/users/'+self.server.domain:
  582. self.path='/inbox'
  583. # show the person options screen with view/follow/block/report
  584. if htmlGET and '/users/' in self.path:
  585. if '?options=' in self.path:
  586. optionsStr=self.path.split('?options=')[1]
  587. originPathStr=self.path.split('?options=')[0]
  588. if ';' in optionsStr:
  589. pageNumber=1
  590. optionsList=optionsStr.split(';')
  591. optionsActor=optionsList[0]
  592. optionsPageNumber=optionsList[1]
  593. optionsProfileUrl=optionsList[2]
  594. if optionsPageNumber.isdigit():
  595. pageNumber=int(optionsPageNumber)
  596. optionsLink=None
  597. if len(optionsList)>3:
  598. optionsLink=optionsList[3]
  599. msg=htmlPersonOptions(self.server.translate, \
  600. self.server.baseDir, \
  601. self.server.domain, \
  602. originPathStr, \
  603. optionsActor, \
  604. optionsProfileUrl, \
  605. optionsLink, \
  606. pageNumber).encode()
  607. self._set_headers('text/html',len(msg),cookie)
  608. self.wfile.write(msg)
  609. self.server.GETbusy=False
  610. return
  611. self._redirect_headers(originPathStr,cookie)
  612. self.server.GETbusy=False
  613. return
  614. # remove a shared item
  615. if htmlGET and '?rmshare=' in self.path:
  616. shareName=self.path.split('?rmshare=')[1]
  617. actor=self.server.httpPrefix+'://'+self.server.domainFull+ \
  618. self.path.split('?rmshare=')[0]
  619. msg=htmlRemoveSharedItem(self.server.translate, \
  620. self.server.baseDir,actor, \
  621. shareName).encode()
  622. if not msg:
  623. self._redirect_headers(actor+'/inbox',cookie)
  624. self.server.GETbusy=False
  625. return
  626. self._set_headers('text/html',len(msg),cookie)
  627. self.wfile.write(msg)
  628. self.server.GETbusy=False
  629. return
  630. if self.path.startswith('/terms'):
  631. msg=htmlTermsOfService(self.server.baseDir, \
  632. self.server.httpPrefix, \
  633. self.server.domainFull).encode()
  634. self._login_headers('text/html',len(msg))
  635. self.wfile.write(msg)
  636. self.server.GETbusy=False
  637. return
  638. if self.path.startswith('/about'):
  639. msg=htmlAbout(self.server.baseDir, \
  640. self.server.httpPrefix, \
  641. self.server.domainFull).encode()
  642. self._login_headers('text/html',len(msg))
  643. self.wfile.write(msg)
  644. self.server.GETbusy=False
  645. return
  646. # send robots.txt if asked
  647. if self._robotsTxt():
  648. self.server.GETbusy=False
  649. return
  650. # if not authorized then show the login screen
  651. if htmlGET and self.path!='/login' and self.path!='/':
  652. if '/media/' not in self.path and \
  653. '/sharefiles/' not in self.path and \
  654. '/statuses/' not in self.path and \
  655. '/emoji/' not in self.path and \
  656. '/tags/' not in self.path and \
  657. '/icons/' not in self.path:
  658. divertToLoginScreen=True
  659. if self.path.startswith('/users/'):
  660. nickStr=self.path.split('/users/')[1]
  661. if '/' not in nickStr and '?' not in nickStr:
  662. divertToLoginScreen=False
  663. else:
  664. if self.path.endswith('/following') or \
  665. self.path.endswith('/followers') or \
  666. self.path.endswith('/skills') or \
  667. self.path.endswith('/roles') or \
  668. self.path.endswith('/shares'):
  669. divertToLoginScreen=False
  670. if divertToLoginScreen and not authorized:
  671. if self.server.debug:
  672. print('DEBUG: divertToLoginScreen='+ \
  673. str(divertToLoginScreen))
  674. print('DEBUG: authorized='+str(authorized))
  675. print('DEBUG: path='+self.path)
  676. self.send_response(303)
  677. self.send_header('Location', '/login')
  678. self.send_header('Content-Length', '0')
  679. self.send_header('X-Robots-Tag','noindex')
  680. self.end_headers()
  681. self.server.GETbusy=False
  682. return
  683. # get css
  684. # Note that this comes before the busy flag to avoid conflicts
  685. if self.path.endswith('.css'):
  686. if os.path.isfile('epicyon-profile.css'):
  687. with open('epicyon-profile.css', 'r') as cssfile:
  688. css = cssfile.read()
  689. msg=css.encode('utf-8')
  690. self._set_headers('text/css',len(msg),cookie)
  691. self.wfile.write(msg)
  692. self.wfile.flush()
  693. return
  694. # image on login screen
  695. if self.path=='/login.png':
  696. mediaFilename= \
  697. self.server.baseDir+'/accounts/login.png'
  698. if os.path.isfile(mediaFilename):
  699. with open(mediaFilename, 'rb') as avFile:
  700. mediaBinary = avFile.read()
  701. self._set_headers('image/png',len(mediaBinary),cookie)
  702. self.wfile.write(mediaBinary)
  703. self.wfile.flush()
  704. self._404()
  705. return
  706. # login screen background image
  707. if self.path=='/login-background.png':
  708. mediaFilename= \
  709. self.server.baseDir+'/accounts/login-background.png'
  710. if os.path.isfile(mediaFilename):
  711. with open(mediaFilename, 'rb') as avFile:
  712. mediaBinary = avFile.read()
  713. self._set_headers('image/png',len(mediaBinary),cookie)
  714. self.wfile.write(mediaBinary)
  715. self.wfile.flush()
  716. return
  717. self._404()
  718. return
  719. # follow screen background image
  720. if self.path=='/follow-background.png':
  721. mediaFilename= \
  722. self.server.baseDir+'/accounts/follow-background.png'
  723. if os.path.isfile(mediaFilename):
  724. with open(mediaFilename, 'rb') as avFile:
  725. mediaBinary = avFile.read()
  726. self._set_headers('image/png',len(mediaBinary),cookie)
  727. self.wfile.write(mediaBinary)
  728. self.wfile.flush()
  729. self._404()
  730. return
  731. # emoji images
  732. if '/emoji/' in self.path:
  733. if self.path.endswith('.png') or \
  734. self.path.endswith('.jpg') or \
  735. self.path.endswith('.gif'):
  736. emojiStr=self.path.split('/emoji/')[1]
  737. emojiFilename= \
  738. self.server.baseDir+'/emoji/'+emojiStr
  739. if os.path.isfile(emojiFilename):
  740. mediaImageType='png'
  741. if emojiFilename.endswith('.png'):
  742. mediaImageType='png'
  743. elif emojiFilename.endswith('.jpg'):
  744. mediaImageType='jpeg'
  745. else:
  746. mediaImageType='gif'
  747. with open(emojiFilename, 'rb') as avFile:
  748. mediaBinary = avFile.read()
  749. self._set_headers('image/'+mediaImageType, \
  750. len(mediaBinary),cookie)
  751. self.wfile.write(mediaBinary)
  752. self.wfile.flush()
  753. return
  754. self._404()
  755. return
  756. # show media
  757. # Note that this comes before the busy flag to avoid conflicts
  758. if '/media/' in self.path:
  759. if self.path.endswith('.png') or \
  760. self.path.endswith('.jpg') or \
  761. self.path.endswith('.gif') or \
  762. self.path.endswith('.mp4') or \
  763. self.path.endswith('.ogv') or \
  764. self.path.endswith('.mp3') or \
  765. self.path.endswith('.ogg'):
  766. mediaStr=self.path.split('/media/')[1]
  767. mediaFilename= \
  768. self.server.baseDir+'/media/'+mediaStr
  769. if os.path.isfile(mediaFilename):
  770. mediaFileType='image/png'
  771. if mediaFilename.endswith('.png'):
  772. mediaFileType='image/png'
  773. elif mediaFilename.endswith('.jpg'):
  774. mediaFileType='image/jpeg'
  775. elif mediaFilename.endswith('.gif'):
  776. mediaFileType='image/gif'
  777. elif mediaFilename.endswith('.mp4'):
  778. mediaFileType='video/mp4'
  779. elif mediaFilename.endswith('.ogv'):
  780. mediaFileType='video/ogv'
  781. elif mediaFilename.endswith('.mp3'):
  782. mediaFileType='audio/mpeg'
  783. elif mediaFilename.endswith('.ogg'):
  784. mediaFileType='audio/ogg'
  785. with open(mediaFilename, 'rb') as avFile:
  786. mediaBinary = avFile.read()
  787. self._set_headers(mediaFileType,len(mediaBinary),cookie)
  788. self.wfile.write(mediaBinary)
  789. self.wfile.flush()
  790. return
  791. self._404()
  792. return
  793. # show shared item images
  794. # Note that this comes before the busy flag to avoid conflicts
  795. if '/sharefiles/' in self.path:
  796. if self.path.endswith('.png') or \
  797. self.path.endswith('.jpg') or \
  798. self.path.endswith('.gif'):
  799. mediaStr=self.path.split('/sharefiles/')[1]
  800. mediaFilename= \
  801. self.server.baseDir+'/sharefiles/'+mediaStr
  802. if os.path.isfile(mediaFilename):
  803. mediaFileType='png'
  804. if mediaFilename.endswith('.png'):
  805. mediaFileType='png'
  806. elif mediaFilename.endswith('.jpg'):
  807. mediaFileType='jpeg'
  808. else:
  809. mediaFileType='gif'
  810. with open(mediaFilename, 'rb') as avFile:
  811. mediaBinary = avFile.read()
  812. self._set_headers('image/'+mediaFileType, \
  813. len(mediaBinary),cookie)
  814. self.wfile.write(mediaBinary)
  815. self.wfile.flush()
  816. return
  817. self._404()
  818. return
  819. # icon images
  820. # Note that this comes before the busy flag to avoid conflicts
  821. if self.path.startswith('/icons/'):
  822. if self.path.endswith('.png'):
  823. mediaStr=self.path.split('/icons/')[1]
  824. mediaFilename= \
  825. self.server.baseDir+'/img/icons/'+mediaStr
  826. if os.path.isfile(mediaFilename):
  827. if mediaFilename.endswith('.png'):
  828. with open(mediaFilename, 'rb') as avFile:
  829. mediaBinary = avFile.read()
  830. self._set_headers('image/png', \
  831. len(mediaBinary),cookie)
  832. self.wfile.write(mediaBinary)
  833. self.wfile.flush()
  834. return
  835. self._404()
  836. return
  837. # show avatar or background image
  838. # Note that this comes before the busy flag to avoid conflicts
  839. if '/users/' in self.path:
  840. if self.path.endswith('.png') or \
  841. self.path.endswith('.jpg') or \
  842. self.path.endswith('.gif'):
  843. avatarStr=self.path.split('/users/')[1]
  844. if '/' in avatarStr:
  845. avatarNickname=avatarStr.split('/')[0]
  846. avatarFile=avatarStr.split('/')[1]
  847. avatarFilename= \
  848. self.server.baseDir+'/accounts/'+ \
  849. avatarNickname+'@'+ \
  850. self.server.domain+'/'+avatarFile
  851. if os.path.isfile(avatarFilename):
  852. mediaImageType='png'
  853. if avatarFile.endswith('.png'):
  854. mediaImageType='png'
  855. elif avatarFile.endswith('.jpg'):
  856. mediaImageType='jpeg'
  857. else:
  858. mediaImageType='gif'
  859. with open(avatarFilename, 'rb') as avFile:
  860. mediaBinary = avFile.read()
  861. self._set_headers('image/'+mediaImageType, \
  862. len(mediaBinary),cookie)
  863. self.wfile.write(mediaBinary)
  864. self.wfile.flush()
  865. return
  866. # This busy state helps to avoid flooding
  867. # Resources which are expected to be called from a web page
  868. # should be above this
  869. if self.server.GETbusy:
  870. currTimeGET=int(time.time())
  871. if currTimeGET-self.server.lastGET==0:
  872. if self.server.debug:
  873. print('DEBUG: GET Busy')
  874. self.send_response(429)
  875. self.end_headers()
  876. return
  877. self.server.lastGET=currTimeGET
  878. self.server.GETbusy=True
  879. if not self._permittedDir(self.path):
  880. if self.server.debug:
  881. print('DEBUG: GET Not permitted')
  882. self._404()
  883. self.server.GETbusy=False
  884. return
  885. # get webfinger endpoint for a person
  886. if self._webfinger():
  887. self.server.GETbusy=False
  888. return
  889. if self.path.startswith('/login') or self.path=='/':
  890. # request basic auth
  891. msg=htmlLogin(self.server.translate, \
  892. self.server.baseDir).encode('utf-8')
  893. self._login_headers('text/html',len(msg))
  894. self.wfile.write(msg)
  895. self.server.GETbusy=False
  896. return
  897. # hashtag search
  898. if self.path.startswith('/tags/'):
  899. pageNumber=1
  900. if '?page=' in self.path:
  901. pageNumberStr=self.path.split('?page=')[1]
  902. if pageNumberStr.isdigit():
  903. pageNumber=int(pageNumberStr)
  904. hashtag=self.path.split('/tags/')[1]
  905. if '?page=' in hashtag:
  906. hashtag=hashtag.split('?page=')[0]
  907. if isBlockedHashtag(self.server.baseDir,hashtag):
  908. msg=htmlHashtagBlocked(self.server.baseDir).encode('utf-8')
  909. self._login_headers('text/html',len(msg))
  910. self.wfile.write(msg)
  911. self.server.GETbusy=False
  912. return
  913. hashtagStr= \
  914. htmlHashtagSearch(self.server.translate, \
  915. self.server.baseDir,hashtag,pageNumber, \
  916. maxPostsInFeed,self.server.session, \
  917. self.server.cachedWebfingers, \
  918. self.server.personCache, \
  919. self.server.httpPrefix, \
  920. self.server.projectVersion)
  921. if hashtagStr:
  922. msg=hashtagStr.encode()
  923. self._set_headers('text/html',len(msg),cookie)
  924. self.wfile.write(msg)
  925. else:
  926. originPathStr=self.path.split('/tags/')[0]
  927. self._redirect_headers(originPathStr+'/search',cookie)
  928. self.server.GETbusy=False
  929. return
  930. # search for a fediverse address, shared item or emoji from
  931. # the web interface by selecting search icon
  932. if htmlGET and '/users/' in self.path:
  933. if self.path.endswith('/search'):
  934. # show the search screen
  935. msg=htmlSearch(self.server.translate, \
  936. self.server.baseDir,self.path).encode()
  937. self._set_headers('text/html',len(msg),cookie)
  938. self.wfile.write(msg)
  939. self.server.GETbusy=False
  940. return
  941. # search for emoji by name
  942. if htmlGET and '/users/' in self.path:
  943. if self.path.endswith('/searchemoji'):
  944. # show the search screen
  945. msg=htmlSearchEmojiTextEntry(self.server.translate, \
  946. self.server.baseDir, \
  947. self.path).encode()
  948. self._set_headers('text/html',len(msg),cookie)
  949. self.wfile.write(msg)
  950. self.server.GETbusy=False
  951. return
  952. # announce/repeat from the web interface
  953. if htmlGET and '?repeat=' in self.path:
  954. pageNumber=1
  955. repeatUrl=self.path.split('?repeat=')[1]
  956. if '?' in repeatUrl:
  957. repeatUrl=repeatUrl.split('?')[0]
  958. if '?page=' in self.path:
  959. pageNumberStr=self.path.split('?page=')[1]
  960. if '?' in pageNumberStr:
  961. pageNumberStr=pageNumberStr.split('?')[0]
  962. if pageNumberStr.isdigit():
  963. pageNumber=int(pageNumberStr)
  964. actor=self.path.split('?repeat=')[0]
  965. self.postToNickname=getNicknameFromActor(actor)
  966. if not self.postToNickname:
  967. print('WARN: unable to find nickname in '+actor)
  968. self.server.GETbusy=False
  969. self._redirect_headers(actor+'/inbox?page='+ \
  970. str(pageNumber),cookie)
  971. return
  972. if not self.server.session:
  973. self.server.session= \
  974. createSession(self.server.domain,self.server.port, \
  975. self.server.useTor)
  976. announceJson= \
  977. createAnnounce(self.server.session, \
  978. self.server.baseDir, \
  979. self.server.federationList, \
  980. self.postToNickname, \
  981. self.server.domain,self.server.port, \
  982. 'https://www.w3.org/ns/activitystreams#Public', \
  983. None,self.server.httpPrefix, \
  984. repeatUrl,False,False, \
  985. self.server.sendThreads, \
  986. self.server.postLog, \
  987. self.server.personCache, \
  988. self.server.cachedWebfingers, \
  989. self.server.debug, \
  990. self.server.projectVersion)
  991. if announceJson:
  992. self._postToOutboxThread(announceJson)
  993. self.server.GETbusy=False
  994. self._redirect_headers(actor+'/inbox?page='+str(pageNumber),cookie)
  995. return
  996. # undo an announce/repeat from the web interface
  997. if htmlGET and '?unrepeat=' in self.path:
  998. pageNumber=1
  999. repeatUrl=self.path.split('?unrepeat=')[1]
  1000. if '?' in repeatUrl:
  1001. repeatUrl=repeatUrl.split('?')[0]
  1002. if '?page=' in self.path:
  1003. pageNumberStr=self.path.split('?page=')[1]
  1004. if '?' in pageNumberStr:
  1005. pageNumberStr=pageNumberStr.split('?')[0]
  1006. if pageNumberStr.isdigit():
  1007. pageNumber=int(pageNumberStr)
  1008. actor=self.path.split('?unrepeat=')[0]
  1009. self.postToNickname=getNicknameFromActor(actor)
  1010. if not self.postToNickname:
  1011. print('WARN: unable to find nickname in '+actor)
  1012. self.server.GETbusy=False
  1013. self._redirect_headers(actor+'/inbox?page='+str(pageNumber),cookie)
  1014. return
  1015. if not self.server.session:
  1016. self.server.session= \
  1017. createSession(self.server.domain,self.server.port, \
  1018. self.server.useTor)
  1019. undoAnnounceActor= \
  1020. self.server.httpPrefix+'://'+self.server.domainFull+ \
  1021. '/users/'+self.postToNickname
  1022. newUndoAnnounce = {
  1023. "@context": "https://www.w3.org/ns/activitystreams",
  1024. 'actor': undoAnnounceActor,
  1025. 'type': 'Undo',
  1026. 'cc': [undoAnnounceActor+'/followers'],
  1027. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  1028. 'object': {
  1029. 'actor': undoAnnounceActor,
  1030. 'cc': [undoAnnounceActor+'/followers'],
  1031. 'object': repeatUrl,
  1032. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  1033. 'type': 'Announce'
  1034. }
  1035. }
  1036. self._postToOutboxThread(newUndoAnnounce)
  1037. self.server.GETbusy=False
  1038. self._redirect_headers(actor+'/inbox?page='+str(pageNumber),cookie)
  1039. return
  1040. # send a follow request approval from the web interface
  1041. if authorized and '/followapprove=' in self.path and \
  1042. self.path.startswith('/users/'):
  1043. originPathStr=self.path.split('/followapprove=')[0]
  1044. followerNickname=originPathStr.replace('/users/','')
  1045. followingHandle=self.path.split('/followapprove=')[1]
  1046. if '@' in followingHandle:
  1047. if not self.server.session:
  1048. self.server.session= \
  1049. createSession(self.server.domain, \
  1050. self.server.port, \
  1051. self.server.useTor)
  1052. manualApproveFollowRequest(self.server.session, \
  1053. self.server.baseDir, \
  1054. self.server.httpPrefix, \
  1055. followerNickname, \
  1056. self.server.domain, \
  1057. self.server.port, \
  1058. followingHandle, \
  1059. self.server.federationList, \
  1060. self.server.sendThreads, \
  1061. self.server.postLog, \
  1062. self.server.cachedWebfingers, \
  1063. self.server.personCache, \
  1064. self.server.acceptedCaps, \
  1065. self.server.debug, \
  1066. self.server.projectVersion)
  1067. self._redirect_headers(originPathStr,cookie)
  1068. self.server.GETbusy=False
  1069. return
  1070. # deny a follow request from the web interface
  1071. if authorized and '/followdeny=' in self.path and \
  1072. self.path.startswith('/users/'):
  1073. originPathStr=self.path.split('/followdeny=')[0]
  1074. followerNickname=originPathStr.replace('/users/','')
  1075. followingHandle=self.path.split('/followdeny=')[1]
  1076. if '@' in followingHandle:
  1077. manualDenyFollowRequest(self.server.session, \
  1078. self.server.baseDir, \
  1079. self.server.httpPrefix, \
  1080. followerNickname, \
  1081. self.server.domain, \
  1082. self.server.port, \
  1083. followingHandle, \
  1084. self.server.federationList, \
  1085. self.server.sendThreads, \
  1086. self.server.postLog, \
  1087. self.server.cachedWebfingers, \
  1088. self.server.personCache, \
  1089. self.server.debug, \
  1090. self.server.projectVersion)
  1091. self._redirect_headers(originPathStr,cookie)
  1092. self.server.GETbusy=False
  1093. return
  1094. # like from the web interface icon
  1095. if htmlGET and '?like=' in self.path and '/statuses/' in self.path:
  1096. pageNumber=1
  1097. likeUrl=self.path.split('?like=')[1]
  1098. if '?' in likeUrl:
  1099. likeUrl=likeUrl.split('?')[0]
  1100. actor=self.path.split('?like=')[0]
  1101. if '?page=' in self.path:
  1102. pageNumberStr=self.path.split('?page=')[1]
  1103. if '?' in pageNumberStr:
  1104. pageNumberStr=pageNumberStr.split('?')[0]
  1105. if pageNumberStr.isdigit():
  1106. pageNumber=int(pageNumberStr)
  1107. self.postToNickname=getNicknameFromActor(actor)
  1108. if not self.postToNickname:
  1109. print('WARN: unable to find nickname in '+actor)
  1110. self.server.GETbusy=False
  1111. self._redirect_headers(actor+'/inbox?page='+ \
  1112. str(pageNumber),cookie)
  1113. return
  1114. if not self.server.session:
  1115. self.server.session= \
  1116. createSession(self.server.domain,self.server.port, \
  1117. self.server.useTor)
  1118. likeActor= \
  1119. self.server.httpPrefix+'://'+self.server.domainFull+ \
  1120. '/users/'+self.postToNickname
  1121. actorLiked=likeUrl.split('/statuses/')[0]
  1122. likeJson= {
  1123. "@context": "https://www.w3.org/ns/activitystreams",
  1124. 'type': 'Like',
  1125. 'actor': likeActor,
  1126. 'object': likeUrl
  1127. }
  1128. self._postToOutboxThread(likeJson)
  1129. self.server.GETbusy=False
  1130. self._redirect_headers(actor+'/inbox?page='+str(pageNumber),cookie)
  1131. return
  1132. # undo a like from the web interface icon
  1133. if htmlGET and '?unlike=' in self.path and '/statuses/' in self.path:
  1134. pageNumber=1
  1135. likeUrl=self.path.split('?unlike=')[1]
  1136. if '?' in likeUrl:
  1137. likeUrl=likeUrl.split('?')[0]
  1138. if '?page=' in self.path:
  1139. pageNumberStr=self.path.split('?page=')[1]
  1140. if '?' in pageNumberStr:
  1141. pageNumberStr=pageNumberStr.split('?')[0]
  1142. if pageNumberStr.isdigit():
  1143. pageNumber=int(pageNumberStr)
  1144. actor=self.path.split('?unlike=')[0]
  1145. self.postToNickname=getNicknameFromActor(actor)
  1146. if not self.postToNickname:
  1147. print('WARN: unable to find nickname in '+actor)
  1148. self.server.GETbusy=False
  1149. self._redirect_headers(actor+'/inbox?page='+ \
  1150. str(pageNumber),cookie)
  1151. return
  1152. if not self.server.session:
  1153. self.server.session= \
  1154. createSession(self.server.domain,self.server.port, \
  1155. self.server.useTor)
  1156. undoActor= \
  1157. self.server.httpPrefix+'://'+self.server.domainFull+ \
  1158. '/users/'+self.postToNickname
  1159. actorLiked=likeUrl.split('/statuses/')[0]
  1160. undoLikeJson= {
  1161. "@context": "https://www.w3.org/ns/activitystreams",
  1162. 'type': 'Undo',
  1163. 'actor': undoActor,
  1164. 'object': {
  1165. 'type': 'Like',
  1166. 'actor': undoActor,
  1167. 'object': likeUrl
  1168. }
  1169. }
  1170. self._postToOutboxThread(undoLikeJson)
  1171. self.server.GETbusy=False
  1172. self._redirect_headers(actor+'/inbox?page='+str(pageNumber),cookie)
  1173. return
  1174. # delete a post from the web interface icon
  1175. if htmlGET and '?delete=' in self.path:
  1176. pageNumber=1
  1177. if '?page=' in self.path:
  1178. pageNumberStr=self.path.split('?page=')[1]
  1179. if '?' in pageNumberStr:
  1180. pageNumberStr=pageNumberStr.split('?')[0]
  1181. if pageNumberStr.isdigit():
  1182. pageNumber=int(pageNumberStr)
  1183. deleteUrl=self.path.split('?delete=')[1]
  1184. if '?' in deleteUrl:
  1185. deleteUrl=deleteUrl.split('?')[0]
  1186. actor=self.server.httpPrefix+'://'+self.server.domainFull+ \
  1187. self.path.split('?delete=')[0]
  1188. if self.server.allowDeletion or \
  1189. deleteUrl.startswith(actor):
  1190. if self.server.debug:
  1191. print('DEBUG: deleteUrl='+deleteUrl)
  1192. print('DEBUG: actor='+actor)
  1193. if actor not in deleteUrl:
  1194. # You can only delete your own posts
  1195. self.server.GETbusy=False
  1196. self._redirect_headers(actor+'/inbox',cookie)
  1197. return
  1198. self.postToNickname=getNicknameFromActor(actor)
  1199. if not self.postToNickname:
  1200. print('WARN: unable to find nickname in '+actor)
  1201. self.server.GETbusy=False
  1202. self._redirect_headers(actor+'/inbox',cookie)
  1203. return
  1204. if not self.server.session:
  1205. self.server.session= \
  1206. createSession(self.server.domain,self.server.port, \
  1207. self.server.useTor)
  1208. deleteStr= \
  1209. htmlDeletePost(self.server.translate,pageNumber, \
  1210. self.server.session,self.server.baseDir, \
  1211. deleteUrl,self.server.httpPrefix, \
  1212. __version__,self.server.cachedWebfingers, \
  1213. self.server.personCache)
  1214. if deleteStr:
  1215. self._set_headers('text/html',len(deleteStr),cookie)
  1216. self.wfile.write(deleteStr.encode())
  1217. self.server.GETbusy=False
  1218. return
  1219. self.server.GETbusy=False
  1220. self._redirect_headers(actor+'/inbox',cookie)
  1221. return
  1222. # reply from the web interface icon
  1223. inReplyToUrl=None
  1224. replyWithDM=False
  1225. replyToList=[]
  1226. replyPageNumber=1
  1227. shareDescription=None
  1228. if htmlGET:
  1229. # public reply
  1230. if '?replyto=' in self.path:
  1231. inReplyToUrl=self.path.split('?replyto=')[1]
  1232. if '?' in inReplyToUrl:
  1233. mentionsList=inReplyToUrl.split('?')
  1234. for m in mentionsList:
  1235. if m.startswith('mention='):
  1236. replyToList.append(m.replace('mention=',''))
  1237. if m.startswith('page='):
  1238. replyPageStr=m.replace('page=','')
  1239. if replyPageStr.isdigit():
  1240. replyPageNumber=int(replyPageStr)
  1241. inReplyToUrl=mentionsList[0]
  1242. self.path=self.path.split('?replyto=')[0]+'/newpost'
  1243. if self.server.debug:
  1244. print('DEBUG: replyto path '+self.path)
  1245. # reply to followers
  1246. if '?replyfollowers=' in self.path:
  1247. inReplyToUrl=self.path.split('?replyfollowers=')[1]
  1248. if '?' in inReplyToUrl:
  1249. mentionsList=inReplyToUrl.split('?')
  1250. for m in mentionsList:
  1251. if m.startswith('mention='):
  1252. replyToList.append(m.replace('mention=',''))
  1253. if m.startswith('page='):
  1254. replyPageStr=m.replace('page=','')
  1255. if replyPageStr.isdigit():
  1256. replyPageNumber=int(replyPageStr)
  1257. inReplyToUrl=mentionsList[0]
  1258. self.path=self.path.split('?replyfollowers=')[0]+'/newfollowers'
  1259. if self.server.debug:
  1260. print('DEBUG: replyfollowers path '+self.path)
  1261. # replying as a direct message, for moderation posts or the dm timeline
  1262. if '?replydm=' in self.path:
  1263. inReplyToUrl=self.path.split('?replydm=')[1]
  1264. if '?' in inReplyToUrl:
  1265. mentionsList=inReplyToUrl.split('?')
  1266. for m in mentionsList:
  1267. if m.startswith('mention='):
  1268. replyToList.append(m.replace('mention=',''))
  1269. if m.startswith('page='):
  1270. replyPageStr=m.replace('page=','')
  1271. if replyPageStr.isdigit():
  1272. replyPageNumber=int(replyPageStr)
  1273. inReplyToUrl=mentionsList[0]
  1274. if inReplyToUrl.startswith('sharedesc:'):
  1275. shareDescription= \
  1276. inReplyToUrl.replace('sharedesc:','').replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%23','#')
  1277. self.path=self.path.split('?replydm=')[0]+'/newdm'
  1278. if self.server.debug:
  1279. print('DEBUG: replydm path '+self.path)
  1280. # edit profile in web interface
  1281. if '/users/' in self.path and self.path.endswith('/editprofile'):
  1282. msg=htmlEditProfile(self.server.translate, \
  1283. self.server.baseDir, \
  1284. self.path,self.server.domain, \
  1285. self.server.port).encode()
  1286. self._set_headers('text/html',len(msg),cookie)
  1287. self.wfile.write(msg)
  1288. self.server.GETbusy=False
  1289. return
  1290. # Various types of new post in the web interface
  1291. if '/users/' in self.path and \
  1292. (self.path.endswith('/newpost') or \
  1293. self.path.endswith('/newunlisted') or \
  1294. self.path.endswith('/newfollowers') or \
  1295. self.path.endswith('/newdm') or \
  1296. self.path.endswith('/newreport') or \
  1297. self.path.endswith('/newshare')):
  1298. msg=htmlNewPost(self.server.translate, \
  1299. self.server.baseDir, \
  1300. self.path,inReplyToUrl, \
  1301. replyToList, \
  1302. shareDescription, \
  1303. replyPageNumber).encode()
  1304. self._set_headers('text/html',len(msg),cookie)
  1305. self.wfile.write(msg)
  1306. self.server.GETbusy=False
  1307. return
  1308. # get an individual post from the path /@nickname/statusnumber
  1309. if '/@' in self.path:
  1310. namedStatus=self.path.split('/@')[1]
  1311. if '/' not in namedStatus:
  1312. # show actor
  1313. nickname=namedStatus
  1314. else:
  1315. postSections=namedStatus.split('/')
  1316. if len(postSections)==2:
  1317. nickname=postSections[0]
  1318. statusNumber=postSections[1]
  1319. if len(statusNumber)>10 and statusNumber.isdigit():
  1320. postFilename= \
  1321. self.server.baseDir+'/accounts/'+nickname+'@'+ \
  1322. self.server.domain+'/outbox/'+ \
  1323. self.server.httpPrefix+':##'+ \
  1324. self.server.domainFull+'#users#'+nickname+ \
  1325. '#statuses#'+statusNumber+'.json'
  1326. if os.path.isfile(postFilename):
  1327. postJsonObject={}
  1328. with open(postFilename, 'r') as fp:
  1329. postJsonObject=commentjson.load(fp)
  1330. # Only authorized viewers get to see likes on
  1331. # posts Otherwize marketers could gain more
  1332. # social graph info
  1333. if not authorized:
  1334. if postJsonObject.get('likes'):
  1335. postJsonObject['likes']={'items': []}
  1336. if self._requestHTTP():
  1337. msg= \
  1338. htmlIndividualPost(self.server.translate, \
  1339. self.server.session, \
  1340. self.server.cachedWebfingers, \
  1341. self.server.personCache, \
  1342. nickname,self.server.domain, \
  1343. self.server.port, \
  1344. authorized,postJsonObject, \
  1345. self.server.httpPrefix, \
  1346. self.server.projectVersion).encode('utf-8')
  1347. self._set_headers('text/html',len(msg),cookie)
  1348. self.wfile.write(msg)
  1349. else:
  1350. msg=json.dumps(postJsonObject).encode('utf-8')
  1351. self._set_headers('application/json', \
  1352. len(msg),None)
  1353. self.wfile.write(msg)
  1354. self.server.GETbusy=False
  1355. return
  1356. else:
  1357. self._404()
  1358. self.server.GETbusy=False
  1359. return
  1360. # get replies to a post /users/nickname/statuses/number/replies
  1361. if self.path.endswith('/replies') or '/replies?page=' in self.path:
  1362. if '/statuses/' in self.path and '/users/' in self.path:
  1363. namedStatus=self.path.split('/users/')[1]
  1364. if '/' in namedStatus:
  1365. postSections=namedStatus.split('/')
  1366. if len(postSections)>=4:
  1367. if postSections[3].startswith('replies'):
  1368. nickname=postSections[0]
  1369. statusNumber=postSections[2]
  1370. if len(statusNumber)>10 and statusNumber.isdigit():
  1371. #get the replies file
  1372. boxname='outbox'
  1373. postDir=self.server.baseDir+'/accounts/'+ \
  1374. nickname+'@'+self.server.domain+'/'+boxname
  1375. postRepliesFilename= \
  1376. postDir+'/'+ \
  1377. self.server.httpPrefix+':##'+ \
  1378. self.server.domainFull+'#users#'+nickname+ \
  1379. '#statuses#'+statusNumber+'.replies'
  1380. if not os.path.isfile(postRepliesFilename):
  1381. # There are no replies, so show empty collection
  1382. repliesJson = {
  1383. '@context': 'https://www.w3.org/ns/activitystreams',
  1384. 'first': self.server.httpPrefix+'://'+ \
  1385. self.server.domainFull+'/users/'+nickname+ \
  1386. '/statuses/'+statusNumber+'/replies?page=true',
  1387. 'id': self.server.httpPrefix+'://'+ \
  1388. self.server.domainFull+'/users/'+nickname+ \
  1389. '/statuses/'+statusNumber+'/replies',
  1390. 'last': self.server.httpPrefix+'://'+ \
  1391. self.server.domainFull+'/users/'+nickname+ \
  1392. '/statuses/'+statusNumber+'/replies?page=true',
  1393. 'totalItems': 0,
  1394. 'type': 'OrderedCollection'}
  1395. if self._requestHTTP():
  1396. if not self.server.session:
  1397. if self.server.debug:
  1398. print('DEBUG: creating new session')
  1399. self.server.session= \
  1400. createSession(self.server.domain, \
  1401. self.server.port, \
  1402. self.server.useTor)
  1403. msg=htmlPostReplies(self.server.translate, \
  1404. self.server.baseDir, \
  1405. self.server.session, \
  1406. self.server.cachedWebfingers, \
  1407. self.server.personCache, \
  1408. nickname, \
  1409. self.server.domain, \
  1410. self.server.port, \
  1411. repliesJson, \
  1412. self.server.httpPrefix, \
  1413. self.server.projectVersion).encode('utf-8')
  1414. self._set_headers('text/html',len(msg),cookie)
  1415. print('----------------------------------------------------')
  1416. pprint(repliesJson)
  1417. self.wfile.write(msg)
  1418. else:
  1419. msg=json.dumps(repliesJson).encode('utf-8')
  1420. self._set_headers('application/json', \
  1421. len(msg),None)
  1422. self.wfile.write(msg)
  1423. self.server.GETbusy=False
  1424. return
  1425. else:
  1426. # replies exist. Itterate through the text file containing message ids
  1427. repliesJson = {
  1428. '@context': 'https://www.w3.org/ns/activitystreams',
  1429. 'id': self.server.httpPrefix+'://'+ \
  1430. self.server.domainFull+'/users/'+nickname+ \
  1431. '/statuses/'+statusNumber+'?page=true',
  1432. 'orderedItems': [
  1433. ],
  1434. 'partOf': self.server.httpPrefix+'://'+ \
  1435. self.server.domainFull+'/users/'+nickname+ \
  1436. '/statuses/'+statusNumber,
  1437. 'type': 'OrderedCollectionPage'}
  1438. # populate the items list with replies
  1439. populateRepliesJson(self.server.baseDir, \
  1440. nickname, \
  1441. self.server.domain, \
  1442. postRepliesFilename, \
  1443. authorized, \
  1444. repliesJson)
  1445. # send the replies json
  1446. if self._requestHTTP():
  1447. if not self.server.session:
  1448. if self.server.debug:
  1449. print('DEBUG: creating new session')
  1450. self.server.session= \
  1451. createSession(self.server.domain, \
  1452. self.server.port, \
  1453. self.server.useTor)
  1454. msg=htmlPostReplies(self.server.translate, \
  1455. self.server.baseDir, \
  1456. self.server.session, \
  1457. self.server.cachedWebfingers, \
  1458. self.server.personCache, \
  1459. nickname, \
  1460. self.server.domain, \
  1461. self.server.port, \
  1462. repliesJson, \
  1463. self.server.httpPrefix, \
  1464. self.server.projectVersion).encode('utf-8')
  1465. self._set_headers('text/html',len(msg),cookie)
  1466. self.wfile.write(msg)
  1467. else:
  1468. msg=json.dumps(repliesJson).encode('utf-8')
  1469. self._set_headers('application/json',len(msg),None)
  1470. self.wfile.write(msg)
  1471. self.server.GETbusy=False
  1472. return
  1473. if self.path.endswith('/roles') and '/users/' in self.path:
  1474. namedStatus=self.path.split('/users/')[1]
  1475. if '/' in namedStatus:
  1476. postSections=namedStatus.split('/')
  1477. nickname=postSections[0]
  1478. actorFilename= \
  1479. self.server.baseDir+'/accounts/'+nickname+'@'+ \
  1480. self.server.domain+'.json'
  1481. if os.path.isfile(actorFilename):
  1482. with open(actorFilename, 'r') as fp:
  1483. actorJson=commentjson.load(fp)
  1484. if actorJson.get('roles'):
  1485. if self._requestHTTP():
  1486. getPerson = \
  1487. personLookup(self.server.domain, \
  1488. self.path.replace('/roles',''), \
  1489. self.server.baseDir)
  1490. if getPerson:
  1491. msg=htmlProfile(self.server.translate, \
  1492. self.server.projectVersion, \
  1493. self.server.baseDir, \
  1494. self.server.httpPrefix, \
  1495. True, \
  1496. self.server.ocapAlways, \
  1497. getPerson,'roles', \
  1498. self.server.session, \
  1499. self.server.cachedWebfingers, \
  1500. self.server.personCache, \
  1501. actorJson['roles'], \
  1502. None,None).encode('utf-8')
  1503. self._set_headers('text/html',len(msg),cookie)
  1504. self.wfile.write(msg)
  1505. else:
  1506. msg=json.dumps(actorJson['roles']).encode('utf-8')
  1507. self._set_headers('application/json',len(msg),None)
  1508. self.wfile.write(msg)
  1509. self.server.GETbusy=False
  1510. return
  1511. # show skills on the profile page
  1512. if self.path.endswith('/skills') and '/users/' in self.path:
  1513. namedStatus=self.path.split('/users/')[1]
  1514. if '/' in namedStatus:
  1515. postSections=namedStatus.split('/')
  1516. nickname=postSections[0]
  1517. actorFilename= \
  1518. self.server.baseDir+'/accounts/'+nickname+'@'+ \
  1519. self.server.domain+'.json'
  1520. if os.path.isfile(actorFilename):
  1521. with open(actorFilename, 'r') as fp:
  1522. actorJson=commentjson.load(fp)
  1523. if actorJson.get('skills'):
  1524. if self._requestHTTP():
  1525. getPerson = \
  1526. personLookup(self.server.domain, \
  1527. self.path.replace('/skills',''), \
  1528. self.server.baseDir)
  1529. if getPerson:
  1530. msg=htmlProfile(self.server.translate, \
  1531. self.server.projectVersion, \
  1532. self.server.baseDir, \
  1533. self.server.httpPrefix, \
  1534. True, \
  1535. self.server.ocapAlways, \
  1536. getPerson,'skills', \
  1537. self.server.session, \
  1538. self.server.cachedWebfingers, \
  1539. self.server.personCache, \
  1540. actorJson['skills'], \
  1541. None,None).encode('utf-8')
  1542. self._set_headers('text/html',len(msg),cookie)
  1543. self.wfile.write(msg)
  1544. else:
  1545. msg=json.dumps(actorJson['skills']).encode('utf-8')
  1546. self._set_headers('application/json',len(msg),None)
  1547. self.wfile.write(msg)
  1548. self.server.GETbusy=False
  1549. return
  1550. actor=self.path.replace('/skills','')
  1551. self._redirect_headers(actor,cookie)
  1552. self.server.GETbusy=False
  1553. return
  1554. # get an individual post from the path /users/nickname/statuses/number
  1555. if '/statuses/' in self.path and '/users/' in self.path:
  1556. namedStatus=self.path.split('/users/')[1]
  1557. if '/' in namedStatus:
  1558. postSections=namedStatus.split('/')
  1559. if len(postSections)>=3:
  1560. nickname=postSections[0]
  1561. statusNumber=postSections[2]
  1562. if len(statusNumber)>10 and statusNumber.isdigit():
  1563. postFilename= \
  1564. self.server.baseDir+'/accounts/'+nickname+'@'+ \
  1565. self.server.domain+'/outbox/'+ \
  1566. self.server.httpPrefix+':##'+ \
  1567. self.server.domainFull+'#users#'+nickname+ \
  1568. '#statuses#'+statusNumber+'.json'
  1569. if os.path.isfile(postFilename):
  1570. postJsonObject={}
  1571. readPost=False
  1572. try:
  1573. with open(postFilename, 'r') as fp:
  1574. postJsonObject=commentjson.load(fp)
  1575. readPost=True
  1576. except Exception as e:
  1577. print(e)
  1578. if not readPost:
  1579. self.send_response(429)
  1580. self.end_headers()
  1581. self.server.GETbusy=False
  1582. return
  1583. else:
  1584. # Only authorized viewers get to see likes on posts
  1585. # Otherwize marketers could gain more social graph info
  1586. if not authorized:
  1587. if postJsonObject.get('likes'):
  1588. postJsonObject['likes']={'items': []}
  1589. if self._requestHTTP():
  1590. msg=htmlIndividualPost(self.server.translate, \
  1591. self.server.baseDir, \
  1592. self.server.session, \
  1593. self.server.cachedWebfingers, \
  1594. self.server.personCache, \
  1595. nickname,self.server.domain, \
  1596. self.server.port, \
  1597. authorized,postJsonObject, \
  1598. self.server.httpPrefix, \
  1599. self.server.projectVersion).encode('utf-8')
  1600. self._set_headers('text/html',len(msg),cookie)
  1601. self.wfile.write(msg)
  1602. else:
  1603. msg=json.dumps(postJsonObject).encode('utf-8')
  1604. self._set_headers('application/json',len(msg),None)
  1605. self.wfile.write(msg)
  1606. self.server.GETbusy=False
  1607. return
  1608. else:
  1609. self._404()
  1610. self.server.GETbusy=False
  1611. return
  1612. # get the inbox for a given person
  1613. if self.path.endswith('/inbox') or '/inbox?page=' in self.path:
  1614. if '/users/' in self.path:
  1615. if authorized:
  1616. inboxFeed=personBoxJson(self.server.baseDir, \
  1617. self.server.domain, \
  1618. self.server.port, \
  1619. self.path, \
  1620. self.server.httpPrefix, \
  1621. maxPostsInFeed, 'inbox', \
  1622. True,self.server.ocapAlways)
  1623. if inboxFeed:
  1624. if self._requestHTTP():
  1625. nickname= \
  1626. self.path.replace('/users/','').replace('/inbox','')
  1627. pageNumber=1
  1628. if '?page=' in nickname:
  1629. pageNumber=nickname.split('?page=')[1]
  1630. nickname=nickname.split('?page=')[0]
  1631. if pageNumber.isdigit():
  1632. pageNumber=int(pageNumber)
  1633. else:
  1634. pageNumber=1
  1635. if 'page=' not in self.path:
  1636. # if no page was specified then show the first
  1637. inboxFeed=personBoxJson(self.server.baseDir, \
  1638. self.server.domain, \
  1639. self.server.port, \
  1640. self.path+'?page=1', \
  1641. self.server.httpPrefix, \
  1642. maxPostsInFeed, 'inbox', \
  1643. True,self.server.ocapAlways)
  1644. msg=htmlInbox(self.server.translate, \
  1645. pageNumber,maxPostsInFeed, \
  1646. self.server.session, \
  1647. self.server.baseDir, \
  1648. self.server.cachedWebfingers, \
  1649. self.server.personCache, \
  1650. nickname, \
  1651. self.server.domain, \
  1652. self.server.port, \
  1653. inboxFeed, \
  1654. self.server.allowDeletion, \
  1655. self.server.httpPrefix, \
  1656. self.server.projectVersion).encode('utf-8')
  1657. self._set_headers('text/html',len(msg),cookie)
  1658. self.wfile.write(msg)
  1659. else:
  1660. msg=json.dumps(inboxFeed).encode('utf-8')
  1661. self._set_headers('application/json',len(msg),None)
  1662. self.wfile.write(msg)
  1663. self.server.GETbusy=False
  1664. return
  1665. else:
  1666. if self.server.debug:
  1667. nickname=self.path.replace('/users/','').replace('/inbox','')
  1668. print('DEBUG: '+nickname+ \
  1669. ' was not authorized to access '+self.path)
  1670. if self.path!='/inbox':
  1671. # not the shared inbox
  1672. if self.server.debug:
  1673. print('DEBUG: GET access to inbox is unauthorized')
  1674. self.send_response(405)
  1675. self.end_headers()
  1676. self.server.GETbusy=False
  1677. return
  1678. # get the inbox for a given person
  1679. if self.path.endswith('/dm') or '/dm?page=' in self.path:
  1680. if '/users/' in self.path:
  1681. if authorized:
  1682. inboxDMFeed=personBoxJson(self.server.baseDir, \
  1683. self.server.domain, \
  1684. self.server.port, \
  1685. self.path, \
  1686. self.server.httpPrefix, \
  1687. maxPostsInFeed, 'dm', \
  1688. True,self.server.ocapAlways)
  1689. if inboxDMFeed:
  1690. if self._requestHTTP():
  1691. nickname= \
  1692. self.path.replace('/users/','').replace('/dm','')
  1693. pageNumber=1
  1694. if '?page=' in nickname:
  1695. pageNumber=nickname.split('?page=')[1]
  1696. nickname=nickname.split('?page=')[0]
  1697. if pageNumber.isdigit():
  1698. pageNumber=int(pageNumber)
  1699. else:
  1700. pageNumber=1
  1701. if 'page=' not in self.path:
  1702. # if no page was specified then show the first
  1703. inboxDMFeed=personBoxJson(self.server.baseDir, \
  1704. self.server.domain, \
  1705. self.server.port, \
  1706. self.path+'?page=1', \
  1707. self.server.httpPrefix, \
  1708. maxPostsInFeed, 'dm', \
  1709. True,self.server.ocapAlways)
  1710. msg=htmlInboxDMs(self.server.translate, \
  1711. pageNumber,maxPostsInFeed, \
  1712. self.server.session, \
  1713. self.server.baseDir, \
  1714. self.server.cachedWebfingers, \
  1715. self.server.personCache, \
  1716. nickname, \
  1717. self.server.domain, \
  1718. self.server.port, \
  1719. inboxDMFeed, \
  1720. self.server.allowDeletion, \
  1721. self.server.httpPrefix, \
  1722. self.server.projectVersion).encode('utf-8')
  1723. self._set_headers('text/html',len(msg),cookie)
  1724. self.wfile.write(msg)
  1725. else:
  1726. msg=json.dumps(inboxDMFeed).encode('utf-8')
  1727. self._set_headers('application/json',len(msg),None)
  1728. self.wfile.write(msg)
  1729. self.server.GETbusy=False
  1730. return
  1731. else:
  1732. if self.server.debug:
  1733. nickname=self.path.replace('/users/','').replace('/dm','')
  1734. print('DEBUG: '+nickname+ \
  1735. ' was not authorized to access '+self.path)
  1736. if self.path!='/dm':
  1737. # not the DM inbox
  1738. if self.server.debug:
  1739. print('DEBUG: GET access to inbox is unauthorized')
  1740. self.send_response(405)
  1741. self.end_headers()
  1742. self.server.GETbusy=False
  1743. return
  1744. # get outbox feed for a person
  1745. outboxFeed=personBoxJson(self.server.baseDir,self.server.domain, \
  1746. self.server.port,self.path, \
  1747. self.server.httpPrefix, \
  1748. maxPostsInFeed, 'outbox', \
  1749. authorized, \
  1750. self.server.ocapAlways)
  1751. if outboxFeed:
  1752. if self._requestHTTP():
  1753. nickname=self.path.replace('/users/','').replace('/outbox','')
  1754. pageNumber=1
  1755. if '?page=' in nickname:
  1756. pageNumber=nickname.split('?page=')[1]
  1757. nickname=nickname.split('?page=')[0]
  1758. if pageNumber.isdigit():
  1759. pageNumber=int(pageNumber)
  1760. else:
  1761. pageNumber=1
  1762. if 'page=' not in self.path:
  1763. # if a page wasn't specified then show the first one
  1764. outboxFeed=personBoxJson(self.server.baseDir, \
  1765. self.server.domain, \
  1766. self.server.port, \
  1767. self.path+'?page=1', \
  1768. self.server.httpPrefix, \
  1769. maxPostsInFeed, 'outbox', \
  1770. authorized, \
  1771. self.server.ocapAlways)
  1772. msg=htmlOutbox(self.server.translate, \
  1773. pageNumber,maxPostsInFeed, \
  1774. self.server.session, \
  1775. self.server.baseDir, \
  1776. self.server.cachedWebfingers, \
  1777. self.server.personCache, \
  1778. nickname, \
  1779. self.server.domain, \
  1780. self.server.port, \
  1781. outboxFeed, \
  1782. self.server.allowDeletion, \
  1783. self.server.httpPrefix, \
  1784. self.server.projectVersion).encode('utf-8')
  1785. self._set_headers('text/html',len(msg),cookie)
  1786. self.wfile.write(msg)
  1787. else:
  1788. msg=json.dumps(outboxFeed).encode('utf-8')
  1789. self._set_headers('application/json',len(msg),None)
  1790. self.wfile.write(msg)
  1791. self.server.GETbusy=False
  1792. return
  1793. # get the moderation feed for a moderator
  1794. if self.path.endswith('/moderation') or \
  1795. '/moderation?page=' in self.path:
  1796. if '/users/' in self.path:
  1797. if authorized:
  1798. moderationFeed= \
  1799. personBoxJson(self.server.baseDir, \
  1800. self.server.domain, \
  1801. self.server.port, \
  1802. self.path, \
  1803. self.server.httpPrefix, \
  1804. maxPostsInFeed, 'moderation', \
  1805. True,self.server.ocapAlways)
  1806. if moderationFeed:
  1807. if self._requestHTTP():
  1808. nickname= \
  1809. self.path.replace('/users/','').replace('/moderation','')
  1810. pageNumber=1
  1811. if '?page=' in nickname:
  1812. pageNumber=nickname.split('?page=')[1]
  1813. nickname=nickname.split('?page=')[0]
  1814. if pageNumber.isdigit():
  1815. pageNumber=int(pageNumber)
  1816. else:
  1817. pageNumber=1
  1818. if 'page=' not in self.path:
  1819. # if no page was specified then show the first
  1820. moderationFeed= \
  1821. personBoxJson(self.server.baseDir, \
  1822. self.server.domain, \
  1823. self.server.port, \
  1824. self.path+'?page=1', \
  1825. self.server.httpPrefix, \
  1826. maxPostsInFeed, 'moderation', \
  1827. True,self.server.ocapAlways)
  1828. msg=htmlModeration(self.server.translate, \
  1829. pageNumber,maxPostsInFeed, \
  1830. self.server.session, \
  1831. self.server.baseDir, \
  1832. self.server.cachedWebfingers, \
  1833. self.server.personCache, \
  1834. nickname, \
  1835. self.server.domain, \
  1836. self.server.port, \
  1837. moderationFeed, \
  1838. True, \
  1839. self.server.httpPrefix, \
  1840. self.server.projectVersion).encode('utf-8')
  1841. self._set_headers('text/html',len(msg),cookie)
  1842. self.wfile.write(msg)
  1843. else:
  1844. msg=json.dumps(moderationFeed).encode('utf-8')
  1845. self._set_headers('application/json',len(msg),None)
  1846. self.wfile.write(msg)
  1847. self.server.GETbusy=False
  1848. return
  1849. else:
  1850. if self.server.debug:
  1851. nickname= \
  1852. self.path.replace('/users/','').replace('/moderation','')
  1853. print('DEBUG: '+nickname+ \
  1854. ' was not authorized to access '+self.path)
  1855. if self.server.debug:
  1856. print('DEBUG: GET access to moderation feed is unauthorized')
  1857. self.send_response(405)
  1858. self.end_headers()
  1859. self.server.GETbusy=False
  1860. return
  1861. shares=getSharesFeedForPerson(self.server.baseDir, \
  1862. self.server.domain, \
  1863. self.server.port,self.path, \
  1864. self.server.httpPrefix, \
  1865. sharesPerPage)
  1866. if shares:
  1867. if self._requestHTTP():
  1868. pageNumber=1
  1869. if '?page=' not in self.path:
  1870. searchPath=self.path
  1871. # get a page of shares, not the summary
  1872. shares=getSharesFeedForPerson(self.server.baseDir, \
  1873. self.server.domain, \
  1874. self.server.port, \
  1875. self.path+'?page=true', \
  1876. self.server.httpPrefix, \
  1877. sharesPerPage)
  1878. else:
  1879. pageNumberStr=self.path.split('?page=')[1]
  1880. if pageNumberStr.isdigit():
  1881. pageNumber=int(pageNumberStr)
  1882. searchPath=self.path.split('?page=')[0]
  1883. getPerson = \
  1884. personLookup(self.server.domain, \
  1885. searchPath.replace('/shares',''), \
  1886. self.server.baseDir)
  1887. if getPerson:
  1888. if not self.server.session:
  1889. if self.server.debug:
  1890. print('DEBUG: creating new session')
  1891. self.server.session= \
  1892. createSession(self.server.domain, \
  1893. self.server.port,self.server.useTor)
  1894. msg=htmlProfile(self.server.translate, \
  1895. self.server.projectVersion, \
  1896. self.server.baseDir, \
  1897. self.server.httpPrefix, \
  1898. authorized, \
  1899. self.server.ocapAlways, \
  1900. getPerson,'shares', \
  1901. self.server.session, \
  1902. self.server.cachedWebfingers, \
  1903. self.server.personCache, \
  1904. shares, \
  1905. pageNumber,sharesPerPage).encode('utf-8')
  1906. self._set_headers('text/html',len(msg),cookie)
  1907. self.wfile.write(msg)
  1908. self.server.GETbusy=False
  1909. return
  1910. else:
  1911. msg=json.dumps(shares).encode('utf-8')
  1912. self._set_headers('application/json',len(msg),None)
  1913. self.wfile.write(msg)
  1914. self.server.GETbusy=False
  1915. return
  1916. following=getFollowingFeed(self.server.baseDir,self.server.domain, \
  1917. self.server.port,self.path, \
  1918. self.server.httpPrefix, \
  1919. authorized,followsPerPage)
  1920. if following:
  1921. if self._requestHTTP():
  1922. pageNumber=1
  1923. if '?page=' not in self.path:
  1924. searchPath=self.path
  1925. # get a page of following, not the summary
  1926. following=getFollowingFeed(self.server.baseDir, \
  1927. self.server.domain, \
  1928. self.server.port, \
  1929. self.path+'?page=true', \
  1930. self.server.httpPrefix, \
  1931. authorized,followsPerPage)
  1932. else:
  1933. pageNumberStr=self.path.split('?page=')[1]
  1934. if pageNumberStr.isdigit():
  1935. pageNumber=int(pageNumberStr)
  1936. searchPath=self.path.split('?page=')[0]
  1937. getPerson = \
  1938. personLookup(self.server.domain, \
  1939. searchPath.replace('/following',''), \
  1940. self.server.baseDir)
  1941. if getPerson:
  1942. if not self.server.session:
  1943. if self.server.debug:
  1944. print('DEBUG: creating new session')
  1945. self.server.session= \
  1946. createSession(self.server.domain, \
  1947. self.server.port,self.server.useTor)
  1948. msg=htmlProfile(self.server.translate, \
  1949. self.server.projectVersion, \
  1950. self.server.baseDir, \
  1951. self.server.httpPrefix, \
  1952. authorized, \
  1953. self.server.ocapAlways, \
  1954. getPerson,'following', \
  1955. self.server.session, \
  1956. self.server.cachedWebfingers, \
  1957. self.server.personCache, \
  1958. following, \
  1959. pageNumber,followsPerPage).encode('utf-8')
  1960. self._set_headers('text/html',len(msg),cookie)
  1961. self.wfile.write(msg)
  1962. self.server.GETbusy=False
  1963. return
  1964. else:
  1965. msg=json.dumps(following).encode('utf-8')
  1966. self._set_headers('application/json',len(msg),None)
  1967. self.wfile.write(msg)
  1968. self.server.GETbusy=False
  1969. return
  1970. followers=getFollowingFeed(self.server.baseDir,self.server.domain, \
  1971. self.server.port,self.path, \
  1972. self.server.httpPrefix, \
  1973. authorized,followsPerPage,'followers')
  1974. if followers:
  1975. if self._requestHTTP():
  1976. pageNumber=1
  1977. if '?page=' not in self.path:
  1978. searchPath=self.path
  1979. # get a page of followers, not the summary
  1980. followers=getFollowingFeed(self.server.baseDir, \
  1981. self.server.domain, \
  1982. self.server.port, \
  1983. self.path+'?page=1', \
  1984. self.server.httpPrefix, \
  1985. authorized,followsPerPage,'followers')
  1986. else:
  1987. pageNumberStr=self.path.split('?page=')[1]
  1988. if pageNumberStr.isdigit():
  1989. pageNumber=int(pageNumberStr)
  1990. searchPath=self.path.split('?page=')[0]
  1991. getPerson = \
  1992. personLookup(self.server.domain, \
  1993. searchPath.replace('/followers',''), \
  1994. self.server.baseDir)
  1995. if getPerson:
  1996. if not self.server.session:
  1997. if self.server.debug:
  1998. print('DEBUG: creating new session')
  1999. self.server.session= \
  2000. createSession(self.server.domain, \
  2001. self.server.port, \
  2002. self.server.useTor)
  2003. msg=htmlProfile(self.server.translate, \
  2004. self.server.projectVersion, \
  2005. self.server.baseDir, \
  2006. self.server.httpPrefix, \
  2007. authorized, \
  2008. self.server.ocapAlways, \
  2009. getPerson,'followers', \
  2010. self.server.session, \
  2011. self.server.cachedWebfingers, \
  2012. self.server.personCache, \
  2013. followers, \
  2014. pageNumber,followsPerPage).encode('utf-8')
  2015. self._set_headers('text/html',len(msg),cookie)
  2016. self.wfile.write(msg)
  2017. self.server.GETbusy=False
  2018. return
  2019. else:
  2020. msg=json.dumps(followers).encode('utf-8')
  2021. self._set_headers('application/json',len(msg),None)
  2022. self.wfile.write(msg)
  2023. self.server.GETbusy=False
  2024. return
  2025. # look up a person
  2026. getPerson = personLookup(self.server.domain,self.path, \
  2027. self.server.baseDir)
  2028. if getPerson:
  2029. if self._requestHTTP():
  2030. if not self.server.session:
  2031. if self.server.debug:
  2032. print('DEBUG: creating new session')
  2033. self.server.session= \
  2034. createSession(self.server.domain, \
  2035. self.server.port, \
  2036. self.server.useTor)
  2037. msg=htmlProfile(self.server.translate, \
  2038. self.server.projectVersion, \
  2039. self.server.baseDir, \
  2040. self.server.httpPrefix, \
  2041. authorized, \
  2042. self.server.ocapAlways, \
  2043. getPerson,'posts',
  2044. self.server.session, \
  2045. self.server.cachedWebfingers, \
  2046. self.server.personCache, \
  2047. None,None).encode('utf-8')
  2048. self._set_headers('text/html',len(msg),cookie)
  2049. self.wfile.write(msg)
  2050. else:
  2051. msg=json.dumps(getPerson).encode('utf-8')
  2052. self._set_headers('application/json',len(msg),None)
  2053. self.wfile.write(msg)
  2054. self.server.GETbusy=False
  2055. return
  2056. # check that a json file was requested
  2057. if not self.path.endswith('.json'):
  2058. if self.server.debug:
  2059. print('DEBUG: GET Not json: '+self.path+' '+self.server.baseDir)
  2060. self._404()
  2061. self.server.GETbusy=False
  2062. return
  2063. # check that the file exists
  2064. filename=self.server.baseDir+self.path
  2065. if os.path.isfile(filename):
  2066. with open(filename, 'r', encoding='utf-8') as File:
  2067. content = File.read()
  2068. contentJson=json.loads(content)
  2069. msg=json.dumps(contentJson).encode('utf-8')
  2070. self._set_headers('application/json',len(msg),None)
  2071. self.wfile.write(msg)
  2072. else:
  2073. if self.server.debug:
  2074. print('DEBUG: GET Unknown file')
  2075. self._404()
  2076. self.server.GETbusy=False
  2077. def do_HEAD(self) -> None:
  2078. self._set_headers('application/json',0,None)
  2079. def _receiveNewPost(self,authorized: bool,postType: str) -> (int,int):
  2080. # 0 = this is not a new post
  2081. # 1 = new post success
  2082. # -1 = new post failed
  2083. # 2 = new post canceled
  2084. pageNumber=1
  2085. if authorized and '/users/' in self.path and \
  2086. '?'+postType+'?' in self.path:
  2087. if '?page=' in self.path:
  2088. pageNumberStr=self.path.split('?page=')[1]
  2089. if '?' in pageNumberStr:
  2090. pageNumberStr=pageNumberStr.split('?')[0]
  2091. if pageNumberStr.isdigit():
  2092. pageNumber=int(pageNumberStr)
  2093. self.path=self.path.split('?page=')[0]
  2094. if ' boundary=' in self.headers['Content-type']:
  2095. nickname=None
  2096. nicknameStr=self.path.split('/users/')[1]
  2097. if '/' in nicknameStr:
  2098. nickname=nicknameStr.split('/')[0]
  2099. else:
  2100. return -1,pageNumber
  2101. length = int(self.headers['Content-length'])
  2102. if length>self.server.maxPostLength:
  2103. print('POST size too large')
  2104. return -1,pageNumber
  2105. boundary=self.headers['Content-type'].split('boundary=')[1]
  2106. if ';' in boundary:
  2107. boundary=boundary.split(';')[0]
  2108. # Note: we don't use cgi here because it's due to be deprecated
  2109. # in Python 3.8/3.10
  2110. # Instead we use the multipart mime parser from the email module
  2111. postBytes=self.rfile.read(length)
  2112. msg = email.parser.BytesParser().parsebytes(postBytes)
  2113. # why don't we just use msg.is_multipart(),
  2114. # rather than splitting? TL;DR it doesn't work for this
  2115. # use case because we're not using email style encoding
  2116. # message/rfc822
  2117. messageFields=msg.get_payload(decode=False).split(boundary)
  2118. fields={}
  2119. filename=None
  2120. attachmentMediaType=None
  2121. for f in messageFields:
  2122. if f=='--':
  2123. continue
  2124. if ' name="' in f:
  2125. postStr=f.split(' name="',1)[1]
  2126. if '"' in postStr:
  2127. postKey=postStr.split('"',1)[0]
  2128. postValueStr=postStr.split('"',1)[1]
  2129. if ';' not in postValueStr:
  2130. if '\r\n' in postValueStr:
  2131. postLines=postValueStr.split('\r\n')
  2132. postValue=''
  2133. if len(postLines)>2:
  2134. for line in range(2,len(postLines)-1):
  2135. if line>2:
  2136. postValue+='\n'
  2137. postValue+=postLines[line]
  2138. fields[postKey]=postValue
  2139. else:
  2140. # directly search the binary array for the beginning
  2141. # of an image
  2142. extensionList=['png','jpeg','gif','mp4','webm','ogv','mp3','ogg']
  2143. for extension in extensionList:
  2144. searchStr=b'Content-Type: image/png'
  2145. if extension=='jpeg':
  2146. searchStr=b'Content-Type: image/jpeg'
  2147. elif extension=='gif':
  2148. searchStr=b'Content-Type: image/gif'
  2149. elif extension=='mp4':
  2150. searchStr=b'Content-Type: video/mp4'
  2151. elif extension=='ogv':
  2152. searchStr=b'Content-Type: video/ogv'
  2153. elif extension=='mp3':
  2154. searchStr=b'Content-Type: audio/mpeg'
  2155. elif extension=='ogg':
  2156. searchStr=b'Content-Type: audio/ogg'
  2157. imageLocation=postBytes.find(searchStr)
  2158. filenameBase= \
  2159. self.server.baseDir+'/accounts/'+ \
  2160. nickname+'@'+self.server.domain+'/upload'
  2161. if imageLocation>-1:
  2162. if extension=='jpeg':
  2163. extension='jpg'
  2164. if extension=='mpeg':
  2165. extension='mp3'
  2166. filename=filenameBase+'.'+extension
  2167. attachmentMediaType= \
  2168. searchStr.decode().split('/')[0].replace('Content-Type: ','')
  2169. break
  2170. if filename and imageLocation>-1:
  2171. # locate the beginning of the image, after any
  2172. # carriage returns
  2173. startPos=imageLocation+len(searchStr)
  2174. for offset in range(1,8):
  2175. if postBytes[startPos+offset]!=10:
  2176. if postBytes[startPos+offset]!=13:
  2177. startPos+=offset
  2178. break
  2179. fd = open(filename, 'wb')
  2180. fd.write(postBytes[startPos:])
  2181. fd.close()
  2182. else:
  2183. filename=None
  2184. # send the post
  2185. if not fields.get('message') and \
  2186. not fields.get('imageDescription'):
  2187. return -1,pageNumber
  2188. if fields.get('submitPost'):
  2189. if fields['submitPost']!='Submit':
  2190. return -1,pageNumber
  2191. else:
  2192. return 2,pageNumber
  2193. if not fields.get('imageDescription'):
  2194. fields['imageDescription']=None
  2195. if not fields.get('subject'):
  2196. fields['subject']=None
  2197. if not fields.get('replyTo'):
  2198. fields['replyTo']=None
  2199. if postType=='newpost':
  2200. messageJson= \
  2201. createPublicPost(self.server.baseDir, \
  2202. nickname, \
  2203. self.server.domain,self.server.port, \
  2204. self.server.httpPrefix, \
  2205. fields['message'],False,False,False, \
  2206. filename,attachmentMediaType, \
  2207. fields['imageDescription'],True, \
  2208. fields['replyTo'], \
  2209. fields['replyTo'],fields['subject'])
  2210. if messageJson:
  2211. self.postToNickname=nickname
  2212. if self._postToOutbox(messageJson,__version__):
  2213. populateReplies(self.server.baseDir, \
  2214. self.server.httpPrefix, \
  2215. self.server.domainFull, \
  2216. messageJson, \
  2217. self.server.maxReplies, \
  2218. self.server.debug)
  2219. return 1,pageNumber
  2220. else:
  2221. return -1,pageNumber
  2222. if postType=='newunlisted':
  2223. messageJson= \
  2224. createUnlistedPost(self.server.baseDir, \
  2225. nickname, \
  2226. self.server.domain,self.server.port, \
  2227. self.server.httpPrefix, \
  2228. fields['message'],False,False,False, \
  2229. filename,attachmentMediaType, \
  2230. fields['imageDescription'],True, \
  2231. fields['replyTo'], \
  2232. fields['replyTo'],fields['subject'])
  2233. if messageJson:
  2234. self.postToNickname=nickname
  2235. if self._postToOutbox(messageJson,__version__):
  2236. populateReplies(self.server.baseDir, \
  2237. self.server.httpPrefix, \
  2238. self.server.domain, \
  2239. messageJson, \
  2240. self.server.maxReplies, \
  2241. self.server.debug)
  2242. return 1,pageNumber
  2243. else:
  2244. return -1,pageNumber
  2245. if postType=='newfollowers':
  2246. messageJson= \
  2247. createFollowersOnlyPost(self.server.baseDir, \
  2248. nickname, \
  2249. self.server.domain,self.server.port, \
  2250. self.server.httpPrefix, \
  2251. fields['message'],True,False,False, \
  2252. filename,attachmentMediaType, \
  2253. fields['imageDescription'],True, \
  2254. fields['replyTo'], \
  2255. fields['replyTo'],fields['subject'])
  2256. if messageJson:
  2257. self.postToNickname=nickname
  2258. if self._postToOutbox(messageJson,__version__):
  2259. populateReplies(self.server.baseDir, \
  2260. self.server.httpPrefix, \
  2261. self.server.domain, \
  2262. messageJson, \
  2263. self.server.maxReplies, \
  2264. self.server.debug)
  2265. return 1,pageNumber
  2266. else:
  2267. return -1,pageNumber
  2268. if postType=='newdm':
  2269. messageJson=None
  2270. if '@' in fields['message']:
  2271. messageJson= \
  2272. createDirectMessagePost(self.server.baseDir, \
  2273. nickname, \
  2274. self.server.domain, \
  2275. self.server.port, \
  2276. self.server.httpPrefix, \
  2277. fields['message'], \
  2278. True,False,False, \
  2279. filename, \
  2280. attachmentMediaType, \
  2281. fields['imageDescription'], \
  2282. True, \
  2283. fields['replyTo'], \
  2284. fields['replyTo'], \
  2285. fields['subject'], \
  2286. self.server.debug)
  2287. if messageJson:
  2288. self.postToNickname=nickname
  2289. if self.server.debug:
  2290. print('DEBUG: new DM to '+ \
  2291. str(messageJson['object']['to']))
  2292. if self._postToOutbox(messageJson,__version__):
  2293. populateReplies(self.server.baseDir, \
  2294. self.server.httpPrefix, \
  2295. self.server.domain, \
  2296. messageJson, \
  2297. self.server.maxReplies, \
  2298. self.server.debug)
  2299. return 1,pageNumber
  2300. else:
  2301. return -1,pageNumber
  2302. if postType=='newreport':
  2303. if attachmentMediaType:
  2304. if attachmentMediaType!='image':
  2305. return -1,pageNumber
  2306. # So as to be sure that this only goes to moderators
  2307. # and not accounts being reported we disable any
  2308. # included fediverse addresses by replacing '@' with '-at-'
  2309. fields['message']=fields['message'].replace('@','-at-')
  2310. messageJson= \
  2311. createReportPost(self.server.baseDir, \
  2312. nickname, \
  2313. self.server.domain,self.server.port, \
  2314. self.server.httpPrefix, \
  2315. fields['message'],True,False,False, \
  2316. filename,attachmentMediaType, \
  2317. fields['imageDescription'],True, \
  2318. self.server.debug,fields['subject'])
  2319. if messageJson:
  2320. self.postToNickname=nickname
  2321. if self._postToOutbox(messageJson,__version__):
  2322. return 1,pageNumber
  2323. else:
  2324. return -1,pageNumber
  2325. if postType=='newshare':
  2326. if not fields.get('itemType'):
  2327. return -1,pageNumber
  2328. if not fields.get('category'):
  2329. return -1,pageNumber
  2330. if not fields.get('location'):
  2331. return -1,pageNumber
  2332. if not fields.get('duration'):
  2333. return -1,pageNumber
  2334. if attachmentMediaType:
  2335. if attachmentMediaType!='image':
  2336. return -1,pageNumber
  2337. addShare(self.server.baseDir, \
  2338. self.server.httpPrefix, \
  2339. nickname, \
  2340. self.server.domain,self.server.port, \
  2341. fields['subject'], \
  2342. fields['message'], \
  2343. filename, \
  2344. fields['itemType'], \
  2345. fields['category'], \
  2346. fields['location'], \
  2347. fields['duration'],
  2348. self.server.debug)
  2349. if filename:
  2350. if os.path.isfile(filename):
  2351. os.remove(filename)
  2352. self.postToNickname=nickname
  2353. return 1,pageNumber
  2354. return -1,pageNumber
  2355. else:
  2356. return 0,pageNumber
  2357. def do_POST(self):
  2358. if not self.server.session:
  2359. self.server.session= \
  2360. createSession(self.server.domain,self.server.port, \
  2361. self.server.useTor)
  2362. if self.server.debug:
  2363. print('DEBUG: POST to from '+self.server.baseDir+ \
  2364. ' path: '+self.path+' busy: '+ \
  2365. str(self.server.POSTbusy))
  2366. if self.server.POSTbusy:
  2367. currTimePOST=int(time.time())
  2368. if currTimePOST-self.server.lastPOST==0:
  2369. self.send_response(429)
  2370. self.end_headers()
  2371. return
  2372. self.server.lastPOST=currTimePOST
  2373. self.server.POSTbusy=True
  2374. if not self.headers.get('Content-type'):
  2375. print('Content-type header missing')
  2376. self.send_response(400)
  2377. self.end_headers()
  2378. self.server.POSTbusy=False
  2379. return
  2380. # remove any trailing slashes from the path
  2381. if not self.path.endswith('confirm'):
  2382. self.path= \
  2383. self.path.replace('/outbox/','/outbox').replace('/inbox/','/inbox').replace('/shares/','/shares').replace('/sharedInbox/','/sharedInbox')
  2384. cookie=None
  2385. if self.headers.get('Cookie'):
  2386. cookie=self.headers['Cookie']
  2387. # check authorization
  2388. authorized = self._isAuthorized()
  2389. if authorized:
  2390. if self.server.debug:
  2391. print('POST Authorization granted')
  2392. else:
  2393. if self.server.debug:
  2394. print('POST Not authorized')
  2395. print(str(self.headers))
  2396. # if this is a POST to teh outbox then check authentication
  2397. self.outboxAuthenticated=False
  2398. self.postToNickname=None
  2399. if self.path.startswith('/login'):
  2400. # get the contents of POST containing login credentials
  2401. length = int(self.headers['Content-length'])
  2402. if length>512:
  2403. print('Login failed - credentials too long')
  2404. self.send_response(401)
  2405. self.end_headers()
  2406. self.server.POSTbusy=False
  2407. return
  2408. loginParams=self.rfile.read(length).decode('utf-8')
  2409. loginNickname,loginPassword,register= \
  2410. htmlGetLoginCredentials(loginParams,self.server.lastLoginTime)
  2411. if loginNickname:
  2412. self.server.lastLoginTime=int(time.time())
  2413. if register:
  2414. if not registerAccount(self.server.baseDir, \
  2415. self.server.httpPrefix, \
  2416. self.server.domain, \
  2417. self.server.port, \
  2418. loginNickname,loginPassword):
  2419. self.server.POSTbusy=False
  2420. self._redirect_headers('/login',cookie)
  2421. return
  2422. authHeader=createBasicAuthHeader(loginNickname,loginPassword)
  2423. if not authorizeBasic(self.server.baseDir, \
  2424. '/users/'+loginNickname+'/outbox', \
  2425. authHeader,False):
  2426. print('Login failed: '+loginNickname)
  2427. # remove any token
  2428. if self.server.tokens.get(loginNickname):
  2429. del self.server.tokensLookup[self.server.tokens[loginNickname]]
  2430. del self.server.tokens[loginNickname]
  2431. del self.server.salts[loginNickname]
  2432. self.send_response(303)
  2433. self.send_header('Content-Length', '0')
  2434. self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
  2435. self.send_header('Location', '/login')
  2436. self.send_header('X-Robots-Tag','noindex')
  2437. self.end_headers()
  2438. self.server.POSTbusy=False
  2439. return
  2440. else:
  2441. if isSuspended(self.server.baseDir,loginNickname):
  2442. msg=htmlSuspended(self.server.baseDir).encode('utf-8')
  2443. self._login_headers('text/html',len(msg))
  2444. self.wfile.write(msg)
  2445. self.server.POSTbusy=False
  2446. return
  2447. # login success - redirect with authorization
  2448. print('Login success: '+loginNickname)
  2449. self.send_response(303)
  2450. # This produces a deterministic token based on nick+password+salt
  2451. # But notice that the salt is ephemeral, so a server reboot changes them.
  2452. # This allows you to be logged in on two or more devices with the
  2453. # same token, but also ensures that if an adversary obtains the token
  2454. # then rebooting the server is sufficient to thwart them, without
  2455. # any password changes.
  2456. if not self.server.salts.get(loginNickname):
  2457. self.server.salts[loginNickname]=createPassword(32)
  2458. self.server.tokens[loginNickname]= \
  2459. sha256((loginNickname+loginPassword+ \
  2460. self.server.salts[loginNickname]).encode('utf-8')).hexdigest()
  2461. self.server.tokensLookup[self.server.tokens[loginNickname]]=loginNickname
  2462. self.send_header('Set-Cookie', \
  2463. 'epicyon='+ \
  2464. self.server.tokens[loginNickname]+ \
  2465. '; SameSite=Strict')
  2466. self.send_header('Location', \
  2467. '/users/'+loginNickname+'/inbox')
  2468. self.send_header('Content-Length', '0')
  2469. self.send_header('X-Robots-Tag','noindex')
  2470. self.end_headers()
  2471. self.server.POSTbusy=False
  2472. return
  2473. self.send_response(200)
  2474. self.end_headers()
  2475. self.server.POSTbusy=False
  2476. return
  2477. # update of profile/avatar from web interface
  2478. if authorized and self.path.endswith('/profiledata'):
  2479. if ' boundary=' in self.headers['Content-type']:
  2480. boundary=self.headers['Content-type'].split('boundary=')[1]
  2481. if ';' in boundary:
  2482. boundary=boundary.split(';')[0]
  2483. actorStr=self.path.replace('/profiledata','').replace('/editprofile','')
  2484. nickname=getNicknameFromActor(actorStr)
  2485. if not nickname:
  2486. print('WARN: nickname not found in '+actorStr)
  2487. self._redirect_headers(actorStr,cookie)
  2488. self.server.POSTbusy=False
  2489. return
  2490. length = int(self.headers['Content-length'])
  2491. if length>self.server.maxPostLength:
  2492. print('Maximum profile data length exceeded '+str(length))
  2493. self._redirect_headers(actorStr,cookie)
  2494. self.server.POSTbusy=False
  2495. return
  2496. postBytes=self.rfile.read(length)
  2497. msg = email.parser.BytesParser().parsebytes(postBytes)
  2498. messageFields=msg.get_payload(decode=False).split(boundary)
  2499. fields={}
  2500. filename=None
  2501. lastImageLocation=0
  2502. for f in messageFields:
  2503. if f=='--':
  2504. continue
  2505. if ' name="' in f:
  2506. postStr=f.split(' name="',1)[1]
  2507. if '"' in postStr:
  2508. postKey=postStr.split('"',1)[0]
  2509. postValueStr=postStr.split('"',1)[1]
  2510. if ';' not in postValueStr:
  2511. if '\r\n' in postValueStr:
  2512. postLines=postValueStr.split('\r\n')
  2513. postValue=''
  2514. if len(postLines)>2:
  2515. for line in range(2,len(postLines)-1):
  2516. if line>2:
  2517. postValue+='\n'
  2518. postValue+=postLines[line]
  2519. fields[postKey]=postValue
  2520. else:
  2521. if 'filename="' not in postStr:
  2522. continue
  2523. filenameStr=postStr.split('filename="')[1]
  2524. if '"' not in filenameStr:
  2525. continue
  2526. postImageFilename=filenameStr.split('"')[0]
  2527. if '.' not in postImageFilename:
  2528. continue
  2529. # directly search the binary array for the beginning
  2530. # of an image
  2531. searchStr=b'Content-Type: image/png'
  2532. imageLocation= \
  2533. postBytes.find(searchStr,lastImageLocation)
  2534. filenameBase= \
  2535. self.server.baseDir+'/accounts/'+ \
  2536. nickname+'@'+self.server.domain+'/'+postKey
  2537. # Note: a .temp extension is used here so that at no time is
  2538. # an image with metadata publicly exposed, even for a few mS
  2539. if imageLocation>-1:
  2540. filename=filenameBase+'.png.temp'
  2541. else:
  2542. searchStr=b'Content-Type: image/jpeg'
  2543. imageLocation= \
  2544. postBytes.find(searchStr, \
  2545. lastImageLocation)
  2546. if imageLocation>-1:
  2547. filename=filenameBase+'.jpg.temp'
  2548. else:
  2549. searchStr=b'Content-Type: image/gif'
  2550. imageLocation= \
  2551. postBytes.find(searchStr, \
  2552. lastImageLocation)
  2553. if imageLocation>-1:
  2554. filename=filenameBase+'.gif.temp'
  2555. if filename and imageLocation>-1:
  2556. # locate the beginning of the image, after any
  2557. # carriage returns
  2558. startPos=imageLocation+len(searchStr)
  2559. for offset in range(1,8):
  2560. if postBytes[startPos+offset]!=10:
  2561. if postBytes[startPos+offset]!=13:
  2562. startPos+=offset
  2563. break
  2564. # look for the end of the image
  2565. imageLocationEnd= \
  2566. postBytes.find(b'-------', \
  2567. imageLocation+1)
  2568. fd = open(filename, 'wb')
  2569. if imageLocationEnd>-1:
  2570. fd.write(postBytes[startPos:][:imageLocationEnd-startPos])
  2571. else:
  2572. fd.write(postBytes[startPos:])
  2573. fd.close()
  2574. # remove exif/metadata
  2575. removeMetaData(filename, \
  2576. filename.replace('.temp',''))
  2577. os.remove(filename)
  2578. lastImageLocation=imageLocation+1
  2579. actorFilename= \
  2580. self.server.baseDir+'/accounts/'+ \
  2581. nickname+'@'+self.server.domain+'.json'
  2582. if os.path.isfile(actorFilename):
  2583. with open(actorFilename, 'r') as fp:
  2584. actorJson=commentjson.load(fp)
  2585. actorChanged=False
  2586. skillCtr=1
  2587. newSkills={}
  2588. while skillCtr<10:
  2589. skillName=fields.get('skillName'+str(skillCtr))
  2590. if not skillName:
  2591. skillCtr+=1
  2592. continue
  2593. skillValue=fields.get('skillValue'+str(skillCtr))
  2594. if not skillValue:
  2595. skillCtr+=1
  2596. continue
  2597. if not actorJson['skills'].get(skillName):
  2598. actorChanged=True
  2599. else:
  2600. if actorJson['skills'][skillName]!= \
  2601. int(skillValue):
  2602. actorChanged=True
  2603. newSkills[skillName]=int(skillValue)
  2604. skillCtr+=1
  2605. if len(actorJson['skills'].items())!= \
  2606. len(newSkills.items()):
  2607. actorChanged=True
  2608. actorJson['skills']=newSkills
  2609. if fields.get('displayNickname'):
  2610. if fields['displayNickname']!=actorJson['name']:
  2611. actorJson['name']=fields['displayNickname']
  2612. actorChanged=True
  2613. if fields.get('bio'):
  2614. if fields['bio']!=actorJson['summary']:
  2615. actorTags={}
  2616. actorJson['summary']= \
  2617. addHtmlTags(self.server.baseDir, \
  2618. self.server.httpPrefix, \
  2619. nickname, \
  2620. self.server.domainFull, \
  2621. fields['bio'],[],actorTags)
  2622. if actorTags:
  2623. actorJson['tag']=[]
  2624. for tagName,tag in actorTags.items():
  2625. actorJson['tag'].append(tag)
  2626. actorChanged=True
  2627. if fields.get('moderators'):
  2628. adminNickname=getConfigParam(self.server.baseDir,'admin')
  2629. if self.path.startswith('/users/'+adminNickname+'/'):
  2630. moderatorsFile= \
  2631. self.server.baseDir+'/accounts/moderators.txt'
  2632. clearModeratorStatus(self.server.baseDir)
  2633. if ',' in fields['moderators']:
  2634. # if the list was given as comma separated
  2635. modFile=open(moderatorsFile,"w+")
  2636. for modNick in fields['moderators'].split(','):
  2637. modNick=modNick.strip()
  2638. if os.path.isdir(self.server.baseDir+ \
  2639. '/accounts/'+modNick+ \
  2640. '@'+self.server.domain):
  2641. modFile.write(modNick+'\n')
  2642. modFile.close()
  2643. for modNick in fields['moderators'].split(','):
  2644. modNick=modNick.strip()
  2645. if os.path.isdir(self.server.baseDir+ \
  2646. '/accounts/'+modNick+ \
  2647. '@'+self.server.domain):
  2648. setRole(self.server.baseDir, \
  2649. modNick, \
  2650. self.server.domain, \
  2651. 'instance','moderator')
  2652. else:
  2653. # nicknames on separate lines
  2654. modFile=open(moderatorsFile,"w+")
  2655. for modNick in fields['moderators'].split('\n'):
  2656. modNick=modNick.strip()
  2657. if os.path.isdir(self.server.baseDir+ \
  2658. '/accounts/'+ \
  2659. modNick+'@'+ \
  2660. self.server.domain):
  2661. modFile.write(modNick+'\n')
  2662. modFile.close()
  2663. for modNick in fields['moderators'].split('\n'):
  2664. modNick=modNick.strip()
  2665. if os.path.isdir(self.server.baseDir+ \
  2666. '/accounts/'+ \
  2667. modNick+'@'+ \
  2668. self.server.domain):
  2669. setRole(self.server.baseDir, \
  2670. modNick, \
  2671. self.server.domain, \
  2672. 'instance','moderator')
  2673. approveFollowers=False
  2674. if fields.get('approveFollowers'):
  2675. if fields['approveFollowers']=='on':
  2676. approveFollowers=True
  2677. if approveFollowers!= \
  2678. actorJson['manuallyApprovesFollowers']:
  2679. actorJson['manuallyApprovesFollowers']= \
  2680. approveFollowers
  2681. actorChanged=True
  2682. if fields.get('isBot'):
  2683. if fields['isBot']=='on':
  2684. if actorJson['type']!='Service':
  2685. actorJson['type']='Service'
  2686. actorChanged=True
  2687. else:
  2688. if actorJson['type']!='Person':
  2689. actorJson['type']='Person'
  2690. actorChanged=True
  2691. # save filtered words list
  2692. filterFilename= \
  2693. self.server.baseDir+'/accounts/'+nickname+ \
  2694. '@'+self.server.domain+'/filters.txt'
  2695. if fields.get('filteredWords'):
  2696. with open(filterFilename, "w") as filterfile:
  2697. filterfile.write(fields['filteredWords'])
  2698. else:
  2699. if os.path.isfile(filterFilename):
  2700. os.remove(filterFilename)
  2701. # save blocked accounts list
  2702. blockedFilename= \
  2703. self.server.baseDir+'/accounts/'+nickname+ \
  2704. '@'+self.server.domain+'/blocking.txt'
  2705. if fields.get('blocked'):
  2706. with open(blockedFilename, "w") as blockedfile:
  2707. blockedfile.write(fields['blocked'])
  2708. else:
  2709. if os.path.isfile(blockedFilename):
  2710. os.remove(blockedFilename)
  2711. # save allowed instances list
  2712. allowedInstancesFilename= \
  2713. self.server.baseDir+'/accounts/'+ \
  2714. nickname+'@'+self.server.domain+ \
  2715. '/allowedinstances.txt'
  2716. if fields.get('allowedInstances'):
  2717. with open(allowedInstancesFilename, "w") as allowedInstancesFile:
  2718. allowedInstancesFile.write(fields['allowedInstances'])
  2719. else:
  2720. if os.path.isfile(allowedInstancesFilename):
  2721. os.remove(allowedInstancesFilename)
  2722. # save actor json file within accounts
  2723. if actorChanged:
  2724. with open(actorFilename, 'w') as fp:
  2725. commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
  2726. # also copy to the actors cache and personCache in memory
  2727. storePersonInCache(self.server.baseDir, \
  2728. actorJson['id'], \
  2729. actorJson, \
  2730. self.server.personCache)
  2731. actorCacheFilename= \
  2732. self.server.baseDir+'/cache/actors/'+ \
  2733. actorJson['id'].replace('/','#')+'.json'
  2734. with open(actorCacheFilename, 'w') as fp:
  2735. commentjson.dump(actorJson, fp, indent=4, sort_keys=False)
  2736. # send actor update to followers
  2737. updateActorJson={
  2738. 'type': 'Update',
  2739. 'actor': actorJson['id'],
  2740. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  2741. 'cc': [actorJson['id']+'/followers'],
  2742. 'object': actorJson
  2743. }
  2744. self.postToNickname=nickname
  2745. self._postToOutboxThread(updateActorJson)
  2746. self._redirect_headers(actorStr,cookie)
  2747. self.server.POSTbusy=False
  2748. return
  2749. # moderator action buttons
  2750. if authorized and '/users/' in self.path and \
  2751. self.path.endswith('/moderationaction'):
  2752. actorStr=self.path.replace('/moderationaction','')
  2753. length = int(self.headers['Content-length'])
  2754. moderationParams=self.rfile.read(length).decode('utf-8')
  2755. print('moderationParams: '+moderationParams)
  2756. if '&' in moderationParams:
  2757. moderationText=None
  2758. moderationButton=None
  2759. for moderationStr in moderationParams.split('&'):
  2760. print('moderationStr: '+moderationStr)
  2761. if moderationStr.startswith('moderationAction'):
  2762. if '=' in moderationStr:
  2763. moderationText=moderationStr.split('=')[1].strip()
  2764. moderationText= \
  2765. moderationText.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').strip()
  2766. elif moderationStr.startswith('submitInfo'):
  2767. msg=htmlModerationInfo(self.server.translate, \
  2768. self.server.baseDir).encode('utf-8')
  2769. self._login_headers('text/html',len(msg))
  2770. self.wfile.write(msg)
  2771. self.server.POSTbusy=False
  2772. return
  2773. elif moderationStr.startswith('submitBlock'):
  2774. moderationButton='block'
  2775. elif moderationStr.startswith('submitUnblock'):
  2776. moderationButton='unblock'
  2777. elif moderationStr.startswith('submitSuspend'):
  2778. moderationButton='suspend'
  2779. elif moderationStr.startswith('submitUnsuspend'):
  2780. moderationButton='unsuspend'
  2781. elif moderationStr.startswith('submitRemove'):
  2782. moderationButton='remove'
  2783. if moderationButton and moderationText:
  2784. if self.server.debug:
  2785. print('moderationButton: '+moderationButton)
  2786. print('moderationText: '+moderationText)
  2787. nickname=moderationText
  2788. if nickname.startswith('http') or \
  2789. nickname.startswith('dat'):
  2790. nickname=getNicknameFromActor(nickname)
  2791. if '@' in nickname:
  2792. nickname=nickname.split('@')[0]
  2793. if moderationButton=='suspend':
  2794. suspendAccount(self.server.baseDir,nickname, \
  2795. self.server.salts)
  2796. if moderationButton=='unsuspend':
  2797. unsuspendAccount(self.server.baseDir,nickname)
  2798. if moderationButton=='block':
  2799. fullBlockDomain=None
  2800. if moderationText.startswith('http') or \
  2801. moderationText.startswith('dat'):
  2802. blockDomain,blockPort= \
  2803. getDomainFromActor(moderationText)
  2804. fullBlockDomain=blockDomain
  2805. if blockPort:
  2806. if blockPort!=80 and blockPort!=443:
  2807. if ':' not in blockDomain:
  2808. fullBlockDomain= \
  2809. blockDomain+':'+str(blockPort)
  2810. if '@' in moderationText:
  2811. fullBlockDomain=moderationText.split('@')[1]
  2812. if fullBlockDomain or nickname.startswith('#'):
  2813. addGlobalBlock(self.server.baseDir, \
  2814. nickname,fullBlockDomain)
  2815. if moderationButton=='unblock':
  2816. fullBlockDomain=None
  2817. if moderationText.startswith('http') or \
  2818. moderationText.startswith('dat'):
  2819. blockDomain,blockPort=getDomainFromActor(moderationText)
  2820. fullBlockDomain=blockDomain
  2821. if blockPort:
  2822. if blockPort!=80 and blockPort!=443:
  2823. if ':' not in blockDomain:
  2824. fullBlockDomain=blockDomain+':'+str(blockPort)
  2825. if '@' in moderationText:
  2826. fullBlockDomain=moderationText.split('@')[1]
  2827. if fullBlockDomain or nickname.startswith('#'):
  2828. removeGlobalBlock(self.server.baseDir, \
  2829. nickname,fullBlockDomain)
  2830. if moderationButton=='remove':
  2831. if '/statuses/' not in moderationText:
  2832. removeAccount(self.server.baseDir, \
  2833. nickname, \
  2834. self.server.domain, \
  2835. self.server.port)
  2836. else:
  2837. # remove a post or thread
  2838. postFilename= \
  2839. locatePost(self.server.baseDir, \
  2840. nickname,self.server.domain, \
  2841. moderationText)
  2842. if postFilename:
  2843. if canRemovePost(self.server.baseDir, \
  2844. nickname, \
  2845. self.server.domain, \
  2846. self.server.port, \
  2847. moderationText):
  2848. deletePost(self.server.baseDir, \
  2849. self.server.httpPrefix, \
  2850. nickname,self.server.omain, \
  2851. postFilename, \
  2852. self.server.debug)
  2853. self._redirect_headers(actorStr+'/moderation',cookie)
  2854. self.server.POSTbusy=False
  2855. return
  2856. searchForEmoji=False
  2857. if self.path.endswith('/searchhandleemoji'):
  2858. searchForEmoji=True
  2859. self.path=self.path.replace('/searchhandleemoji','/searchhandle')
  2860. if self.server.debug:
  2861. print('DEBUG: searching for emoji')
  2862. print('authorized: '+str(authorized))
  2863. # a search was made
  2864. if (authorized or searchForEmoji) and \
  2865. (self.path.endswith('/searchhandle') or \
  2866. '/searchhandle?page=' in self.path):
  2867. # get the page number
  2868. pageNumber=1
  2869. if '/searchhandle?page=' in self.path:
  2870. pageNumberStr=self.path.split('/searchhandle?page=')[1]
  2871. if pageNumberStr.isdigit():
  2872. pageNumber=int(pageNumberStr)
  2873. self.path=self.path.split('?page=')[0]
  2874. actorStr= \
  2875. self.server.httpPrefix+'://'+self.server.domainFull+ \
  2876. self.path.replace('/searchhandle','')
  2877. length = int(self.headers['Content-length'])
  2878. searchParams=self.rfile.read(length).decode('utf-8')
  2879. if 'searchtext=' in searchParams:
  2880. searchStr=searchParams.split('searchtext=')[1]
  2881. if '&' in searchStr:
  2882. searchStr=searchStr.split('&')[0]
  2883. searchStr= \
  2884. searchStr.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').replace('%2F','/').strip()
  2885. if self.server.debug:
  2886. print('searchStr: '+searchStr)
  2887. if searchForEmoji:
  2888. searchStr=':'+searchStr+':'
  2889. if searchStr.startswith('#'):
  2890. # hashtag search
  2891. hashtagStr= \
  2892. htmlHashtagSearch(self.server.translate, \
  2893. self.server.baseDir, \
  2894. searchStr[1:],1, \
  2895. maxPostsInFeed,self.server.session, \
  2896. self.server.cachedWebfingers, \
  2897. self.server.personCache, \
  2898. self.server.httpPrefix, \
  2899. self.server.projectVersion)
  2900. if hashtagStr:
  2901. msg=hashtagStr.encode('utf-8')
  2902. self._login_headers('text/html',len(msg))
  2903. self.wfile.write(msg)
  2904. self.server.POSTbusy=False
  2905. return
  2906. elif searchStr.startswith('*'):
  2907. # skill search
  2908. searchStr=searchStr.replace('*','').strip()
  2909. skillStr= \
  2910. htmlSkillsSearch(self.server.translate, \
  2911. self.server.baseDir,searchStr, \
  2912. self.server.instanceOnlySkillsSearch, \
  2913. 64)
  2914. if skillStr:
  2915. msg=skillStr.encode('utf-8')
  2916. self._login_headers('text/html',len(msg))
  2917. self.wfile.write(msg)
  2918. self.server.POSTbusy=False
  2919. return
  2920. elif '@' in searchStr:
  2921. # profile search
  2922. nickname=getNicknameFromActor(self.path)
  2923. if not self.server.session:
  2924. self.server.session= \
  2925. createSession(self.server.domain, \
  2926. self.server.port, \
  2927. self.server.useTor)
  2928. profileStr= \
  2929. htmlProfileAfterSearch(self.server.translate, \
  2930. self.server.baseDir, \
  2931. self.path.replace('/searchhandle',''), \
  2932. self.server.httpPrefix, \
  2933. nickname, \
  2934. self.server.domain,self.server.port, \
  2935. searchStr, \
  2936. self.server.session, \
  2937. self.server.cachedWebfingers, \
  2938. self.server.personCache, \
  2939. self.server.debug, \
  2940. self.server.projectVersion)
  2941. if profileStr:
  2942. msg=profileStr.encode('utf-8')
  2943. self._login_headers('text/html',len(msg))
  2944. self.wfile.write(msg)
  2945. self.server.POSTbusy=False
  2946. return
  2947. else:
  2948. self._redirect_headers(actorStr+'/search',cookie)
  2949. self.server.POSTbusy=False
  2950. return
  2951. elif searchStr.startswith(':') or \
  2952. searchStr.lower().strip('\n').endswith(' emoji'):
  2953. # eg. "cat emoji"
  2954. if searchStr.lower().strip('\n').endswith(' emoji'):
  2955. searchStr=searchStr.lower().strip('\n').replace(' emoji','')
  2956. # emoji search
  2957. emojiStr= \
  2958. htmlSearchEmoji(self.server.translate, \
  2959. self.server.baseDir,searchStr)
  2960. if emojiStr:
  2961. msg=emojiStr.encode('utf-8')
  2962. self._login_headers('text/html',len(msg))
  2963. self.wfile.write(msg)
  2964. self.server.POSTbusy=False
  2965. return
  2966. else:
  2967. # shared items search
  2968. sharedItemsStr= \
  2969. htmlSearchSharedItems(self.server.translate, \
  2970. self.server.baseDir, \
  2971. searchStr,pageNumber, \
  2972. maxPostsInFeed, \
  2973. self.server.httpPrefix, \
  2974. self.server.domainFull, \
  2975. actorStr)
  2976. if sharedItemsStr:
  2977. msg=sharedItemsStr.encode('utf-8')
  2978. self._login_headers('text/html',len(msg))
  2979. self.wfile.write(msg)
  2980. self.server.POSTbusy=False
  2981. return
  2982. self._redirect_headers(actorStr+'/inbox',cookie)
  2983. self.server.POSTbusy=False
  2984. return
  2985. # removes a shared item
  2986. if authorized and self.path.endswith('/rmshare'):
  2987. originPathStr=self.path.split('/rmshare')[0]
  2988. length = int(self.headers['Content-length'])
  2989. removeShareConfirmParams=self.rfile.read(length).decode('utf-8')
  2990. if '&submitYes=' in removeShareConfirmParams:
  2991. removeShareConfirmParams= \
  2992. removeShareConfirmParams.replace('%3A',':').replace('%2F','/')
  2993. shareActor=removeShareConfirmParams.split('actor=')[1]
  2994. if '&' in shareActor:
  2995. shareActor=shareActor.split('&')[0]
  2996. shareName=removeShareConfirmParams.split('shareName=')[1]
  2997. if '&' in shareName:
  2998. shareName=shareName.split('&')[0]
  2999. shareNickname=getNicknameFromActor(shareActor)
  3000. if shareNickname:
  3001. shareDomain,sharePort=getDomainFromActor(shareActor)
  3002. removeShare(self.server.baseDir,shareNickname, \
  3003. shareDomain,shareName)
  3004. self._redirect_headers(originPathStr+'/inbox',cookie)
  3005. self.server.POSTbusy=False
  3006. return
  3007. # removes a post
  3008. if authorized and self.path.endswith('/rmpost'):
  3009. pageNumber=1
  3010. originPathStr=self.path.split('/rmpost')[0]
  3011. length = int(self.headers['Content-length'])
  3012. removePostConfirmParams=self.rfile.read(length).decode('utf-8')
  3013. if '&submitYes=' in removePostConfirmParams:
  3014. removePostConfirmParams= \
  3015. removePostConfirmParams.replace('%3A',':').replace('%2F','/')
  3016. removeMessageId=removePostConfirmParams.split('messageId=')[1]
  3017. if '&' in removeMessageId:
  3018. removeMessageId=removeMessageId.split('&')[0]
  3019. if 'pageNumber=' in removePostConfirmParams:
  3020. pageNumberStr=removePostConfirmParams.split('pageNumber=')[1]
  3021. if '&' in pageNumberStr:
  3022. pageNumberStr=pageNumberStr.split('&')[0]
  3023. if pageNumberStr.isdigit():
  3024. pageNumber=int(pageNumberStr)
  3025. if '/statuses/' in removeMessageId:
  3026. removePostActor=removeMessageId.split('/statuses/')[0]
  3027. if originPathStr in removePostActor:
  3028. deleteJson= {
  3029. "@context": "https://www.w3.org/ns/activitystreams",
  3030. 'actor': removePostActor,
  3031. 'object': removeMessageId,
  3032. 'to': ['https://www.w3.org/ns/activitystreams#Public',removePostActor],
  3033. 'cc': [removePostActor+'/followers'],
  3034. 'type': 'Delete'
  3035. }
  3036. if self.server.debug:
  3037. pprint(deleteJson)
  3038. self.postToNickname=getNicknameFromActor(removePostActor)
  3039. if self.postToNickname:
  3040. self._postToOutboxThread(deleteJson)
  3041. if pageNumber==1:
  3042. self._redirect_headers(originPathStr+'/outbox',cookie)
  3043. else:
  3044. self._redirect_headers(originPathStr+'/outbox?page='+ \
  3045. str(pageNumber),cookie)
  3046. self.server.POSTbusy=False
  3047. return
  3048. # decision to follow in the web interface is confirmed
  3049. if authorized and self.path.endswith('/followconfirm'):
  3050. originPathStr=self.path.split('/followconfirm')[0]
  3051. followerNickname=getNicknameFromActor(originPathStr)
  3052. length = int(self.headers['Content-length'])
  3053. followConfirmParams=self.rfile.read(length).decode('utf-8')
  3054. if '&submitYes=' in followConfirmParams:
  3055. followingActor= \
  3056. followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  3057. if '&' in followingActor:
  3058. followingActor=followingActor.split('&')[0]
  3059. followingNickname=getNicknameFromActor(followingActor)
  3060. followingDomain,followingPort=getDomainFromActor(followingActor)
  3061. if followerNickname==followingNickname and \
  3062. followingDomain==self.server.domain and \
  3063. followingPort==self.server.port:
  3064. if self.server.debug:
  3065. print('You cannot follow yourself!')
  3066. else:
  3067. if self.server.debug:
  3068. print('Sending follow request from '+ \
  3069. followerNickname+' to '+followingActor)
  3070. sendFollowRequest(self.server.session, \
  3071. self.server.baseDir, \
  3072. followerNickname, \
  3073. self.server.domain,self.server.port, \
  3074. self.server.httpPrefix, \
  3075. followingNickname, \
  3076. followingDomain, \
  3077. followingPort,self.server.httpPrefix, \
  3078. False,self.server.federationList, \
  3079. self.server.sendThreads, \
  3080. self.server.postLog, \
  3081. self.server.cachedWebfingers, \
  3082. self.server.personCache, \
  3083. self.server.debug, \
  3084. self.server.projectVersion)
  3085. self._redirect_headers(originPathStr,cookie)
  3086. self.server.POSTbusy=False
  3087. return
  3088. # decision to unfollow in the web interface is confirmed
  3089. if authorized and self.path.endswith('/unfollowconfirm'):
  3090. originPathStr=self.path.split('/unfollowconfirm')[0]
  3091. followerNickname=getNicknameFromActor(originPathStr)
  3092. length = int(self.headers['Content-length'])
  3093. followConfirmParams=self.rfile.read(length).decode('utf-8')
  3094. if '&submitYes=' in followConfirmParams:
  3095. followingActor= \
  3096. followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  3097. if '&' in followingActor:
  3098. followingActor=followingActor.split('&')[0]
  3099. followingNickname=getNicknameFromActor(followingActor)
  3100. followingDomain,followingPort=getDomainFromActor(followingActor)
  3101. if followerNickname==followingNickname and \
  3102. followingDomain==self.server.domain and \
  3103. followingPort==self.server.port:
  3104. if self.server.debug:
  3105. print('You cannot unfollow yourself!')
  3106. else:
  3107. if self.server.debug:
  3108. print(followerNickname+' stops following '+followingActor)
  3109. followActor= \
  3110. self.server.httpPrefix+'://'+ \
  3111. self.server.domainFull+'/users/'+followerNickname
  3112. statusNumber,published = getStatusNumber()
  3113. followId=followActor+'/statuses/'+str(statusNumber)
  3114. unfollowJson = {
  3115. '@context': 'https://www.w3.org/ns/activitystreams',
  3116. 'id': followId+'/undo',
  3117. 'type': 'Undo',
  3118. 'actor': followActor,
  3119. 'object': {
  3120. 'id': followId,
  3121. 'type': 'Follow',
  3122. 'actor': followActor,
  3123. 'object': followingActor
  3124. }
  3125. }
  3126. pathUsersSection=self.path.split('/users/')[1]
  3127. self.postToNickname=pathUsersSection.split('/')[0]
  3128. self._postToOutboxThread(unfollowJson)
  3129. self._redirect_headers(originPathStr,cookie)
  3130. self.server.POSTbusy=False
  3131. return
  3132. # decision to unblock in the web interface is confirmed
  3133. if authorized and self.path.endswith('/unblockconfirm'):
  3134. originPathStr=self.path.split('/unblockconfirm')[0]
  3135. blockerNickname=getNicknameFromActor(originPathStr)
  3136. if not blockerNickname:
  3137. print('WARN: unable to find nickname in '+originPathStr)
  3138. self._redirect_headers(originPathStr,cookie)
  3139. self.server.POSTbusy=False
  3140. return
  3141. length = int(self.headers['Content-length'])
  3142. blockConfirmParams=self.rfile.read(length).decode('utf-8')
  3143. if '&submitYes=' in blockConfirmParams:
  3144. blockingActor= \
  3145. blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  3146. if '&' in blockingActor:
  3147. blockingActor=blockingActor.split('&')[0]
  3148. blockingNickname=getNicknameFromActor(blockingActor)
  3149. if not blockingNickname:
  3150. print('WARN: unable to find nickname in '+blockingActor)
  3151. self._redirect_headers(originPathStr,cookie)
  3152. self.server.POSTbusy=False
  3153. return
  3154. blockingDomain,blockingPort=getDomainFromActor(blockingActor)
  3155. blockingDomainFull=blockingDomain
  3156. if blockingPort:
  3157. if blockingPort!=80 and blockingPort!=443:
  3158. if ':' not in blockingDomain:
  3159. blockingDomainFull=blockingDomain+':'+str(blockingPort)
  3160. if blockerNickname==blockingNickname and \
  3161. blockingDomain==self.server.domain and \
  3162. blockingPort==self.server.port:
  3163. if self.server.debug:
  3164. print('You cannot unblock yourself!')
  3165. else:
  3166. if self.server.debug:
  3167. print(blockerNickname+' stops blocking '+blockingActor)
  3168. removeBlock(self.server.baseDir,blockerNickname, \
  3169. self.server.domain, \
  3170. blockingNickname,blockingDomainFull)
  3171. self._redirect_headers(originPathStr,cookie)
  3172. self.server.POSTbusy=False
  3173. return
  3174. # decision to block in the web interface is confirmed
  3175. if authorized and self.path.endswith('/blockconfirm'):
  3176. originPathStr=self.path.split('/blockconfirm')[0]
  3177. blockerNickname=getNicknameFromActor(originPathStr)
  3178. if not blockerNickname:
  3179. print('WARN: unable to find nickname in '+originPathStr)
  3180. self._redirect_headers(originPathStr,cookie)
  3181. self.server.POSTbusy=False
  3182. return
  3183. length = int(self.headers['Content-length'])
  3184. blockConfirmParams=self.rfile.read(length).decode('utf-8')
  3185. if '&submitYes=' in blockConfirmParams:
  3186. blockingActor= \
  3187. blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  3188. if '&' in blockingActor:
  3189. blockingActor=blockingActor.split('&')[0]
  3190. blockingNickname=getNicknameFromActor(blockingActor)
  3191. if not blockingNickname:
  3192. print('WARN: unable to find nickname in '+blockingActor)
  3193. self._redirect_headers(originPathStr,cookie)
  3194. self.server.POSTbusy=False
  3195. return
  3196. blockingDomain,blockingPort=getDomainFromActor(blockingActor)
  3197. blockingDomainFull=blockingDomain
  3198. if blockingPort:
  3199. if blockingPort!=80 and blockingPort!=443:
  3200. if ':' not in blockingDomain:
  3201. blockingDomainFull= \
  3202. blockingDomain+':'+str(blockingPort)
  3203. if blockerNickname==blockingNickname and \
  3204. blockingDomain==self.server.domain and \
  3205. blockingPort==self.server.port:
  3206. if self.server.debug:
  3207. print('You cannot block yourself!')
  3208. else:
  3209. if self.server.debug:
  3210. print('Adding block by '+ \
  3211. blockerNickname+' of '+blockingActor)
  3212. addBlock(self.server.baseDir,blockerNickname, \
  3213. self.server.domain, \
  3214. blockingNickname,blockingDomainFull)
  3215. self._redirect_headers(originPathStr,cookie)
  3216. self.server.POSTbusy=False
  3217. return
  3218. # an option was chosen from person options screen
  3219. # view/follow/block/report
  3220. if authorized and self.path.endswith('/personoptions'):
  3221. pageNumber=1
  3222. originPathStr=self.path.split('/personoptions')[0]
  3223. chooserNickname=getNicknameFromActor(originPathStr)
  3224. if not chooserNickname:
  3225. print('WARN: unable to find nickname in '+originPathStr)
  3226. self._redirect_headers(originPathStr,cookie)
  3227. self.server.POSTbusy=False
  3228. return
  3229. length = int(self.headers['Content-length'])
  3230. optionsConfirmParams= \
  3231. self.rfile.read(length).decode('utf-8').replace('%3A',':').replace('%2F','/')
  3232. # page number to return to
  3233. if 'pageNumber=' in optionsConfirmParams:
  3234. pageNumberStr=optionsConfirmParams.split('pageNumber=')[1]
  3235. if '&' in pageNumberStr:
  3236. pageNumberStr=pageNumberStr.split('&')[0]
  3237. if pageNumberStr.isdigit():
  3238. pageNumber=int(pageNumberStr)
  3239. # actor for the person
  3240. optionsActor=optionsConfirmParams.split('actor=')[1]
  3241. if '&' in optionsActor:
  3242. optionsActor=optionsActor.split('&')[0]
  3243. # url of the avatar
  3244. optionsAvatarUrl=optionsConfirmParams.split('avatarUrl=')[1]
  3245. if '&' in optionsAvatarUrl:
  3246. optionsAvatarUrl=optionsAvatarUrl.split('&')[0]
  3247. # link to a post, which can then be included in reports
  3248. postUrl=None
  3249. if 'postUrl' in optionsConfirmParams:
  3250. postUrl=optionsConfirmParams.split('postUrl=')[1]
  3251. if '&' in postUrl:
  3252. postUrl=postUrl.split('&')[0]
  3253. optionsNickname=getNicknameFromActor(optionsActor)
  3254. if not optionsNickname:
  3255. print('WARN: unable to find nickname in '+optionsActor)
  3256. self._redirect_headers(originPathStr,cookie)
  3257. self.server.POSTbusy=False
  3258. return
  3259. optionsDomain,optionsPort=getDomainFromActor(optionsActor)
  3260. optionsDomainFull=optionsDomain
  3261. if optionsPort:
  3262. if optionsPort!=80 and optionsPort!=443:
  3263. if ':' not in optionsDomain:
  3264. optionsDomainFull=optionsDomain+':'+str(optionsPort)
  3265. if chooserNickname==optionsNickname and \
  3266. optionsDomain==self.server.domain and \
  3267. optionsPort==self.server.port:
  3268. if self.server.debug:
  3269. print('You cannot perform an option action on yourself')
  3270. if '&submitView=' in optionsConfirmParams:
  3271. if self.server.debug:
  3272. print('Viewing '+optionsActor)
  3273. self._redirect_headers(optionsActor,cookie)
  3274. self.server.POSTbusy=False
  3275. return
  3276. if '&submitBlock=' in optionsConfirmParams:
  3277. if self.server.debug:
  3278. print('Adding block by '+ \
  3279. chooserNickname+' of '+optionsActor)
  3280. addBlock(self.server.baseDir,chooserNickname, \
  3281. self.server.domain, \
  3282. optionsNickname,optionsDomainFull)
  3283. if '&submitUnblock=' in optionsConfirmParams:
  3284. if self.server.debug:
  3285. print('Unblocking '+optionsActor)
  3286. msg=htmlUnblockConfirm(self.server.translate, \
  3287. self.server.baseDir, \
  3288. originPathStr, \
  3289. optionsActor, \
  3290. optionsAvatarUrl).encode()
  3291. self._set_headers('text/html',len(msg),cookie)
  3292. self.wfile.write(msg)
  3293. self.server.POSTbusy=False
  3294. return
  3295. if '&submitFollow=' in optionsConfirmParams:
  3296. if self.server.debug:
  3297. print('Following '+optionsActor)
  3298. msg=htmlFollowConfirm(self.server.translate, \
  3299. self.server.baseDir, \
  3300. originPathStr, \
  3301. optionsActor, \
  3302. optionsAvatarUrl).encode()
  3303. self._set_headers('text/html',len(msg),cookie)
  3304. self.wfile.write(msg)
  3305. self.server.POSTbusy=False
  3306. return
  3307. if '&submitUnfollow=' in optionsConfirmParams:
  3308. if self.server.debug:
  3309. print('Unfollowing '+optionsActor)
  3310. msg=htmlUnfollowConfirm(self.server.translate, \
  3311. self.server.baseDir, \
  3312. originPathStr, \
  3313. optionsActor, \
  3314. optionsAvatarUrl).encode()
  3315. self._set_headers('text/html',len(msg),cookie)
  3316. self.wfile.write(msg)
  3317. self.server.POSTbusy=False
  3318. return
  3319. if '&submitDM=' in optionsConfirmParams:
  3320. if self.server.debug:
  3321. print('Sending DM to '+optionsActor)
  3322. reportPath=self.path.replace('/personoptions','')+'/newdm'
  3323. msg=htmlNewPost(self.server.translate, \
  3324. self.server.baseDir, \
  3325. reportPath,None, \
  3326. [optionsActor],None, \
  3327. pageNumber).encode()
  3328. self._set_headers('text/html',len(msg),cookie)
  3329. self.wfile.write(msg)
  3330. self.server.POSTbusy=False
  3331. return
  3332. if '&submitReport=' in optionsConfirmParams:
  3333. if self.server.debug:
  3334. print('Reporting '+optionsActor)
  3335. reportPath=self.path.replace('/personoptions','')+'/newreport'
  3336. msg=htmlNewPost(self.server.translate, \
  3337. self.server.baseDir, \
  3338. reportPath,None,[], \
  3339. postUrl,pageNumber).encode()
  3340. self._set_headers('text/html',len(msg),cookie)
  3341. self.wfile.write(msg)
  3342. self.server.POSTbusy=False
  3343. return
  3344. self._redirect_headers(originPathStr,cookie)
  3345. self.server.POSTbusy=False
  3346. return
  3347. postState,pageNumber=self._receiveNewPost(authorized,'newpost')
  3348. if postState!=0:
  3349. nickname=self.path.split('/users/')[1]
  3350. if '/' in nickname:
  3351. nickname=nickname.split('/')[0]
  3352. self._redirect_headers('/users/'+nickname+'/inbox?page='+str(pageNumber),cookie)
  3353. self.server.POSTbusy=False
  3354. return
  3355. postState,pageNumber=self._receiveNewPost(authorized,'newunlisted')
  3356. if postState!=0:
  3357. nickname=self.path.split('/users/')[1]
  3358. if '/' in nickname:
  3359. nickname=nickname.split('/')[0]
  3360. self._redirect_headers('/users/'+nickname+ \
  3361. '/inbox?page='+str(pageNumber),cookie)
  3362. self.server.POSTbusy=False
  3363. return
  3364. postState,pageNumber=self._receiveNewPost(authorized,'newfollowers')
  3365. if postState!=0:
  3366. nickname=self.path.split('/users/')[1]
  3367. if '/' in nickname:
  3368. nickname=nickname.split('/')[0]
  3369. self._redirect_headers('/users/'+ \
  3370. nickname+'/inbox?page='+ \
  3371. str(pageNumber),cookie)
  3372. self.server.POSTbusy=False
  3373. return
  3374. postState,pageNumber=self._receiveNewPost(authorized,'newdm')
  3375. if postState!=0:
  3376. nickname=self.path.split('/users/')[1]
  3377. if '/' in nickname:
  3378. nickname=nickname.split('/')[0]
  3379. self._redirect_headers('/users/'+nickname+'/inbox?page='+ \
  3380. str(pageNumber),cookie)
  3381. self.server.POSTbusy=False
  3382. return
  3383. postState,pageNumber=self._receiveNewPost(authorized,'newreport')
  3384. if postState!=0:
  3385. nickname=self.path.split('/users/')[1]
  3386. if '/' in nickname:
  3387. nickname=nickname.split('/')[0]
  3388. self._redirect_headers('/users/'+nickname+'/inbox?page='+ \
  3389. str(pageNumber),cookie)
  3390. self.server.POSTbusy=False
  3391. return
  3392. postState,pageNumber=self._receiveNewPost(authorized,'newshare')
  3393. if postState!=0:
  3394. nickname=self.path.split('/users/')[1]
  3395. if '/' in nickname:
  3396. nickname=nickname.split('/')[0]
  3397. self._redirect_headers('/users/'+nickname+'/shares?page='+ \
  3398. str(pageNumber),cookie)
  3399. self.server.POSTbusy=False
  3400. return
  3401. if self.path.endswith('/outbox') or self.path.endswith('/shares'):
  3402. if '/users/' in self.path:
  3403. if authorized:
  3404. self.outboxAuthenticated=True
  3405. pathUsersSection=self.path.split('/users/')[1]
  3406. self.postToNickname=pathUsersSection.split('/')[0]
  3407. if not self.outboxAuthenticated:
  3408. self.send_response(405)
  3409. self.end_headers()
  3410. self.server.POSTbusy=False
  3411. return
  3412. # check that the post is to an expected path
  3413. if not (self.path.endswith('/outbox') or \
  3414. self.path.endswith('/inbox') or \
  3415. self.path.endswith('/shares') or \
  3416. self.path.endswith('/moderationaction') or \
  3417. self.path.endswith('/caps/new') or \
  3418. self.path=='/sharedInbox'):
  3419. print('Attempt to POST to invalid path '+self.path)
  3420. self.send_response(400)
  3421. self.end_headers()
  3422. self.server.POSTbusy=False
  3423. return
  3424. # read the message and convert it into a python dictionary
  3425. length = int(self.headers['Content-length'])
  3426. if self.server.debug:
  3427. print('DEBUG: content-length: '+str(length))
  3428. if not self.headers['Content-type'].startswith('image/') and \
  3429. not self.headers['Content-type'].startswith('video/') and \
  3430. not self.headers['Content-type'].startswith('audio/'):
  3431. if length>self.server.maxMessageLength:
  3432. print('Maximum message length exceeded '+str(length))
  3433. self.send_response(400)
  3434. self.end_headers()
  3435. self.server.POSTbusy=False
  3436. return
  3437. else:
  3438. if length>self.server.maxMediaSize:
  3439. print('Maximum media size exceeded '+str(length))
  3440. self.send_response(400)
  3441. self.end_headers()
  3442. self.server.POSTbusy=False
  3443. return
  3444. # receive images to the outbox
  3445. if self.headers['Content-type'].startswith('image/') and \
  3446. '/users/' in self.path:
  3447. if not self.outboxAuthenticated:
  3448. if self.server.debug:
  3449. print('DEBUG: unauthenticated attempt to post image to outbox')
  3450. self.send_response(403)
  3451. self.end_headers()
  3452. self.server.POSTbusy=False
  3453. return
  3454. pathUsersSection=self.path.split('/users/')[1]
  3455. if '/' not in pathUsersSection:
  3456. self.send_response(404)
  3457. self.end_headers()
  3458. self.server.POSTbusy=False
  3459. return
  3460. self.postFromNickname=pathUsersSection.split('/')[0]
  3461. accountsDir= \
  3462. self.server.baseDir+'/accounts/'+ \
  3463. self.postFromNickname+'@'+self.server.domain
  3464. if not os.path.isdir(accountsDir):
  3465. self.send_response(404)
  3466. self.end_headers()
  3467. self.server.POSTbusy=False
  3468. return
  3469. mediaBytes=self.rfile.read(length)
  3470. mediaFilenameBase=accountsDir+'/upload'
  3471. mediaFilename=mediaFilenameBase+'.png'
  3472. if self.headers['Content-type'].endswith('jpeg'):
  3473. mediaFilename=mediaFilenameBase+'.jpg'
  3474. if self.headers['Content-type'].endswith('gif'):
  3475. mediaFilename=mediaFilenameBase+'.gif'
  3476. with open(mediaFilename, 'wb') as avFile:
  3477. avFile.write(mediaBytes)
  3478. if self.server.debug:
  3479. print('DEBUG: image saved to '+mediaFilename)
  3480. self.send_response(201)
  3481. self.end_headers()
  3482. self.server.POSTbusy=False
  3483. return
  3484. # refuse to receive non-json content
  3485. if self.headers['Content-type'] != 'application/json' and \
  3486. self.headers['Content-type'] != 'application/activity+json':
  3487. print("POST is not json: "+self.headers['Content-type'])
  3488. if self.server.debug:
  3489. print(str(self.headers))
  3490. length = int(self.headers['Content-length'])
  3491. if length<self.server.maxPostLength:
  3492. unknownPost=self.rfile.read(length).decode('utf-8')
  3493. print(str(unknownPost))
  3494. self.send_response(400)
  3495. self.end_headers()
  3496. self.server.POSTbusy=False
  3497. return
  3498. if self.server.debug:
  3499. print('DEBUG: Reading message')
  3500. messageBytes=self.rfile.read(length)
  3501. messageJson=json.loads(messageBytes)
  3502. # https://www.w3.org/TR/activitypub/#object-without-create
  3503. if self.outboxAuthenticated:
  3504. if self._postToOutbox(messageJson,__version__):
  3505. if messageJson.get('id'):
  3506. self.headers['Location']= \
  3507. messageJson['id'].replace('/activity','').replace('/undo','')
  3508. self.send_response(201)
  3509. self.end_headers()
  3510. self.server.POSTbusy=False
  3511. return
  3512. else:
  3513. if self.server.debug:
  3514. print('Failed to post to outbox')
  3515. self.send_response(403)
  3516. self.end_headers()
  3517. self.server.POSTbusy=False
  3518. return
  3519. # check the necessary properties are available
  3520. if self.server.debug:
  3521. print('DEBUG: Check message has params')
  3522. if self.path.endswith('/inbox') or \
  3523. self.path=='/sharedInbox':
  3524. if not inboxMessageHasParams(messageJson):
  3525. if self.server.debug:
  3526. pprint(messageJson)
  3527. print("DEBUG: inbox message doesn't have the required parameters")
  3528. self.send_response(403)
  3529. self.end_headers()
  3530. self.server.POSTbusy=False
  3531. return
  3532. if not inboxPermittedMessage(self.server.domain, \
  3533. messageJson, \
  3534. self.server.federationList):
  3535. if self.server.debug:
  3536. # https://www.youtube.com/watch?v=K3PrSj9XEu4
  3537. print('DEBUG: Ah Ah Ah')
  3538. self.send_response(403)
  3539. self.end_headers()
  3540. self.server.POSTbusy=False
  3541. return
  3542. if self.server.debug:
  3543. pprint(messageJson)
  3544. if not self.headers.get('signature'):
  3545. if 'keyId=' not in self.headers['signature']:
  3546. if self.server.debug:
  3547. print('DEBUG: POST to inbox has no keyId in header signature parameter')
  3548. self.send_response(403)
  3549. self.end_headers()
  3550. self.server.POSTbusy=False
  3551. return
  3552. if self.server.debug:
  3553. print('DEBUG: POST saving to inbox queue')
  3554. if '/users/' in self.path:
  3555. pathUsersSection=self.path.split('/users/')[1]
  3556. if '/' not in pathUsersSection:
  3557. if self.server.debug:
  3558. print('DEBUG: This is not a users endpoint')
  3559. else:
  3560. self.postToNickname=pathUsersSection.split('/')[0]
  3561. if self.postToNickname:
  3562. queueStatus= \
  3563. self._updateInboxQueue(self.postToNickname, \
  3564. messageJson,messageBytes)
  3565. if queueStatus==0:
  3566. self.send_response(200)
  3567. self.end_headers()
  3568. self.server.POSTbusy=False
  3569. return
  3570. if queueStatus==1:
  3571. self.send_response(503)
  3572. self.end_headers()
  3573. self.server.POSTbusy=False
  3574. return
  3575. if self.server.debug:
  3576. print('_updateInboxQueue exited without doing anything')
  3577. else:
  3578. if self.server.debug:
  3579. print('self.postToNickname is None')
  3580. self.send_response(403)
  3581. self.end_headers()
  3582. self.server.POSTbusy=False
  3583. return
  3584. else:
  3585. if self.path == '/sharedInbox' or self.path == '/inbox':
  3586. print('DEBUG: POST to shared inbox')
  3587. queueStatus= \
  3588. self._updateInboxQueue('inbox',messageJson, \
  3589. messageBytes)
  3590. if queueStatus==0:
  3591. self.send_response(200)
  3592. self.end_headers()
  3593. self.server.POSTbusy=False
  3594. return
  3595. if queueStatus==1:
  3596. self.send_response(503)
  3597. self.end_headers()
  3598. self.server.POSTbusy=False
  3599. return
  3600. self.send_response(200)
  3601. self.end_headers()
  3602. self.server.POSTbusy=False
  3603. class PubServerUnitTest(PubServer):
  3604. protocol_version = 'HTTP/1.0'
  3605. def runDaemon(projectVersion, \
  3606. instanceId,clientToServer: bool, \
  3607. baseDir: str,domain: str, \
  3608. port=80,proxyPort=80,httpPrefix='https', \
  3609. fedList=[],noreply=False,nolike=False,nopics=False, \
  3610. noannounce=False,cw=False,ocapAlways=False, \
  3611. useTor=False,maxReplies=64, \
  3612. domainMaxPostsPerDay=8640,accountMaxPostsPerDay=8640, \
  3613. allowDeletion=False,debug=False,unitTest=False, \
  3614. instanceOnlySkillsSearch=False) -> None:
  3615. if len(domain)==0:
  3616. domain='localhost'
  3617. if '.' not in domain:
  3618. if domain != 'localhost':
  3619. print('Invalid domain: ' + domain)
  3620. return
  3621. serverAddress = ('', proxyPort)
  3622. if unitTest:
  3623. httpd = ThreadingHTTPServer(serverAddress, PubServerUnitTest)
  3624. else:
  3625. httpd = ThreadingHTTPServer(serverAddress, PubServer)
  3626. # load translations dictionary
  3627. httpd.translate={}
  3628. if not unitTest:
  3629. if not os.path.isdir(baseDir+'/translations'):
  3630. print('ERROR: translations directory not found')
  3631. return
  3632. systemLanguage=locale.getdefaultlocale()[0]
  3633. if '_' in systemLanguage:
  3634. systemLanguage=systemLanguage.split('_')[0]
  3635. if '.' in systemLanguage:
  3636. systemLanguage=systemLanguage.split('.')[0]
  3637. translationsFile=baseDir+'/translations/'+systemLanguage+'.json'
  3638. if not os.path.isfile(translationsFile):
  3639. systemLanguage='en'
  3640. translationsFile=baseDir+'/translations/'+systemLanguage+'.json'
  3641. print('System language: '+systemLanguage)
  3642. with open(translationsFile, 'r') as fp:
  3643. httpd.translate=commentjson.load(fp)
  3644. httpd.outboxThread={}
  3645. httpd.projectVersion=projectVersion
  3646. # max POST size of 30M
  3647. httpd.maxPostLength=1024*1024*30
  3648. httpd.maxMediaSize=httpd.maxPostLength
  3649. httpd.maxMessageLength=5000
  3650. httpd.maxPostsInBox=256
  3651. httpd.domain=domain
  3652. httpd.port=port
  3653. httpd.domainFull=domain
  3654. if port:
  3655. if port!=80 and port!=443:
  3656. if ':' not in domain:
  3657. httpd.domainFull=domain+':'+str(port)
  3658. httpd.httpPrefix=httpPrefix
  3659. httpd.debug=debug
  3660. httpd.federationList=fedList.copy()
  3661. httpd.baseDir=baseDir
  3662. httpd.instanceId=instanceId
  3663. httpd.personCache={}
  3664. httpd.cachedWebfingers={}
  3665. httpd.useTor=useTor
  3666. httpd.session = None
  3667. httpd.sessionLastUpdate=0
  3668. httpd.lastGET=0
  3669. httpd.lastPOST=0
  3670. httpd.GETbusy=False
  3671. httpd.POSTbusy=False
  3672. httpd.receivedMessage=False
  3673. httpd.inboxQueue=[]
  3674. httpd.sendThreads=[]
  3675. httpd.postLog=[]
  3676. httpd.maxQueueLength=16
  3677. httpd.ocapAlways=ocapAlways
  3678. httpd.allowDeletion=allowDeletion
  3679. httpd.lastLoginTime=0
  3680. httpd.maxReplies=maxReplies
  3681. httpd.salts={}
  3682. httpd.tokens={}
  3683. httpd.tokensLookup={}
  3684. httpd.instanceOnlySkillsSearch=instanceOnlySkillsSearch
  3685. httpd.acceptedCaps=["inbox:write","objects:read"]
  3686. if noreply:
  3687. httpd.acceptedCaps.append('inbox:noreply')
  3688. if nolike:
  3689. httpd.acceptedCaps.append('inbox:nolike')
  3690. if nopics:
  3691. httpd.acceptedCaps.append('inbox:nopics')
  3692. if noannounce:
  3693. httpd.acceptedCaps.append('inbox:noannounce')
  3694. if cw:
  3695. httpd.acceptedCaps.append('inbox:cw')
  3696. if not os.path.isdir(baseDir+'/accounts/inbox@'+domain):
  3697. print('Creating shared inbox: inbox@'+domain)
  3698. createSharedInbox(baseDir,'inbox',domain,port,httpPrefix)
  3699. if not os.path.isdir(baseDir+'/cache'):
  3700. os.mkdir(baseDir+'/cache')
  3701. if not os.path.isdir(baseDir+'/cache/actors'):
  3702. print('Creating actors cache')
  3703. os.mkdir(baseDir+'/cache/actors')
  3704. if not os.path.isdir(baseDir+'/cache/announce'):
  3705. print('Creating announce cache')
  3706. os.mkdir(baseDir+'/cache/announce')
  3707. archiveDir=baseDir+'/archive'
  3708. if not os.path.isdir(archiveDir):
  3709. print('Creating archive')
  3710. os.mkdir(archiveDir)
  3711. print('Creating cache expiry thread')
  3712. httpd.thrCache= \
  3713. threadWithTrace(target=expireCache, \
  3714. args=(baseDir,httpd.personCache, \
  3715. httpd.httpPrefix, \
  3716. archiveDir, \
  3717. httpd.maxPostsInBox),daemon=True)
  3718. httpd.thrCache.start()
  3719. print('Creating inbox queue')
  3720. httpd.thrInboxQueue= \
  3721. threadWithTrace(target=runInboxQueue, \
  3722. args=(projectVersion, \
  3723. baseDir,httpPrefix,httpd.sendThreads, \
  3724. httpd.postLog,httpd.cachedWebfingers, \
  3725. httpd.personCache,httpd.inboxQueue, \
  3726. domain,port,useTor,httpd.federationList, \
  3727. httpd.ocapAlways,maxReplies, \
  3728. domainMaxPostsPerDay,accountMaxPostsPerDay, \
  3729. allowDeletion,debug,httpd.acceptedCaps),daemon=True)
  3730. if not unitTest:
  3731. httpd.thrWatchdog= \
  3732. threadWithTrace(target=runInboxQueueWatchdog, \
  3733. args=(projectVersion,httpd),daemon=True)
  3734. httpd.thrWatchdog.start()
  3735. else:
  3736. httpd.thrInboxQueue.start()
  3737. if clientToServer:
  3738. print('Running ActivityPub client on ' + domain + ' port ' + str(proxyPort))
  3739. else:
  3740. print('Running ActivityPub server on ' + domain + ' port ' + str(proxyPort))
  3741. httpd.serve_forever()