daemon.py 189 KB

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