daemon.py 277 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928492949304931493249334934493549364937493849394940494149424943494449454946494749484949495049514952495349544955495649574958495949604961496249634964496549664967496849694970497149724973497449754976497749784979498049814982498349844985498649874988498949904991499249934994499549964997499849995000500150025003500450055006500750085009501050115012501350145015501650175018501950205021502250235024502550265027502850295030503150325033503450355036503750385039504050415042504350445045504650475048504950505051505250535054505550565057505850595060506150625063506450655066506750685069507050715072507350745075507650775078507950805081508250835084508550865087508850895090509150925093509450955096509750985099510051015102510351045105510651075108510951105111511251135114511551165117511851195120512151225123512451255126512751285129513051315132513351345135513651375138513951405141514251435144514551465147514851495150515151525153515451555156515751585159516051615162516351645165516651675168516951705171517251735174517551765177517851795180518151825183518451855186518751885189519051915192519351945195519651975198519952005201520252035204520552065207520852095210521152125213521452155216521752185219522052215222522352245225522652275228522952305231523252335234523552365237523852395240524152425243524452455246524752485249525052515252525352545255525652575258525952605261526252635264526552665267526852695270527152725273527452755276527752785279528052815282528352845285528652875288528952905291529252935294529552965297529852995300530153025303530453055306530753085309531053115312531353145315531653175318531953205321532253235324532553265327532853295330533153325333533453355336533753385339534053415342534353445345534653475348534953505351535253535354535553565357535853595360536153625363536453655366536753685369537053715372537353745375537653775378537953805381538253835384538553865387538853895390539153925393539453955396539753985399540054015402540354045405540654075408540954105411541254135414541554165417541854195420542154225423542454255426542754285429543054315432543354345435543654375438543954405441544254435444544554465447544854495450545154525453545454555456545754585459546054615462546354645465546654675468546954705471547254735474547554765477547854795480548154825483548454855486548754885489549054915492549354945495549654975498549955005501550255035504550555065507550855095510551155125513551455155516551755185519552055215522552355245525552655275528552955305531553255335534553555365537553855395540554155425543554455455546554755485549555055515552555355545555555655575558555955605561556255635564556555665567556855695570557155725573557455755576557755785579558055815582558355845585558655875588558955905591559255935594559555965597559855995600560156025603560456055606560756085609561056115612561356145615561656175618561956205621562256235624562556265627562856295630563156325633563456355636563756385639564056415642564356445645564656475648564956505651565256535654565556565657565856595660566156625663566456655666566756685669567056715672567356745675567656775678567956805681568256835684568556865687568856895690569156925693569456955696569756985699570057015702570357045705570657075708570957105711571257135714571557165717571857195720572157225723572457255726572757285729573057315732573357345735573657375738573957405741574257435744
  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 json
  11. import time
  12. import base64
  13. import locale
  14. # used for mime decoding of message POST
  15. import email.parser
  16. # for saving images
  17. from binascii import a2b_base64
  18. from hashlib import sha256
  19. from hashlib import sha1
  20. from session import createSession
  21. from webfinger import webfingerMeta
  22. from webfinger import webfingerNodeInfo
  23. from webfinger import webfingerLookup
  24. from webfinger import webfingerHandle
  25. from metadata import metaDataNodeInfo
  26. from metadata import metaDataInstance
  27. from donate import getDonationUrl
  28. from donate import setDonationUrl
  29. from person import activateAccount
  30. from person import deactivateAccount
  31. from person import registerAccount
  32. from person import personLookup
  33. from person import personBoxJson
  34. from person import createSharedInbox
  35. from person import isSuspended
  36. from person import suspendAccount
  37. from person import unsuspendAccount
  38. from person import removeAccount
  39. from person import canRemovePost
  40. from person import personSnooze
  41. from person import personUnsnooze
  42. from posts import mutePost
  43. from posts import unmutePost
  44. from posts import createQuestionPost
  45. from posts import outboxMessageCreateWrap
  46. from posts import savePostToBox
  47. from posts import sendToFollowersThread
  48. from posts import postIsAddressedToPublic
  49. from posts import sendToNamedAddresses
  50. from posts import createPublicPost
  51. from posts import createReportPost
  52. from posts import createUnlistedPost
  53. from posts import createFollowersOnlyPost
  54. from posts import createDirectMessagePost
  55. from posts import populateRepliesJson
  56. from posts import addToField
  57. from posts import expireCache
  58. from inbox import inboxPermittedMessage
  59. from inbox import inboxMessageHasParams
  60. from inbox import runInboxQueue
  61. from inbox import runInboxQueueWatchdog
  62. from inbox import savePostToInboxQueue
  63. from inbox import populateReplies
  64. from inbox import getPersonPubKey
  65. from inbox import inboxUpdateIndex
  66. from follow import getFollowingFeed
  67. from follow import outboxUndoFollow
  68. from follow import sendFollowRequest
  69. from auth import authorize
  70. from auth import createPassword
  71. from auth import createBasicAuthHeader
  72. from auth import authorizeBasic
  73. from auth import storeBasicCredentials
  74. from threads import threadWithTrace
  75. from threads import removeDormantThreads
  76. from media import getMediaPath
  77. from media import createMediaDirs
  78. from delete import outboxDelete
  79. from like import outboxLike
  80. from like import outboxUndoLike
  81. from bookmarks import outboxBookmark
  82. from bookmarks import outboxUndoBookmark
  83. from blocking import outboxBlock
  84. from blocking import outboxUndoBlock
  85. from blocking import addBlock
  86. from blocking import removeBlock
  87. from blocking import addGlobalBlock
  88. from blocking import removeGlobalBlock
  89. from blocking import isBlockedHashtag
  90. from blocking import isBlockedDomain
  91. from config import setConfigParam
  92. from config import getConfigParam
  93. from roles import outboxDelegate
  94. from roles import setRole
  95. from roles import clearModeratorStatus
  96. from skills import outboxSkills
  97. from availability import outboxAvailability
  98. from webinterface import htmlDeletePost
  99. from webinterface import htmlAbout
  100. from webinterface import htmlRemoveSharedItem
  101. from webinterface import htmlInboxDMs
  102. from webinterface import htmlInboxReplies
  103. from webinterface import htmlInboxMedia
  104. from webinterface import htmlUnblockConfirm
  105. from webinterface import htmlPersonOptions
  106. from webinterface import htmlIndividualPost
  107. from webinterface import htmlProfile
  108. from webinterface import htmlInbox
  109. from webinterface import htmlBookmarks
  110. from webinterface import htmlShares
  111. from webinterface import htmlOutbox
  112. from webinterface import htmlModeration
  113. from webinterface import htmlPostReplies
  114. from webinterface import htmlLogin
  115. from webinterface import htmlSuspended
  116. from webinterface import htmlGetLoginCredentials
  117. from webinterface import htmlNewPost
  118. from webinterface import htmlFollowConfirm
  119. from webinterface import htmlCalendar
  120. from webinterface import htmlSearch
  121. from webinterface import htmlSearchEmoji
  122. from webinterface import htmlSearchEmojiTextEntry
  123. from webinterface import htmlUnfollowConfirm
  124. from webinterface import htmlProfileAfterSearch
  125. from webinterface import htmlEditProfile
  126. from webinterface import htmlTermsOfService
  127. from webinterface import htmlSkillsSearch
  128. from webinterface import htmlHashtagSearch
  129. from webinterface import htmlModerationInfo
  130. from webinterface import htmlSearchSharedItems
  131. from webinterface import htmlHashtagBlocked
  132. from shares import getSharesFeedForPerson
  133. from shares import outboxShareUpload
  134. from shares import outboxUndoShareUpload
  135. from shares import addShare
  136. from shares import removeShare
  137. from shares import expireShares
  138. from utils import locatePost
  139. from utils import getCachedPostFilename
  140. from utils import removePostFromCache
  141. from utils import getNicknameFromActor
  142. from utils import getDomainFromActor
  143. from utils import getStatusNumber
  144. from utils import urlPermitted
  145. from utils import loadJson
  146. from utils import saveJson
  147. from manualapprove import manualDenyFollowRequest
  148. from manualapprove import manualApproveFollowRequest
  149. from announce import createAnnounce
  150. from announce import outboxAnnounce
  151. from content import addHtmlTags
  152. from content import extractMediaInFormPOST
  153. from content import saveMediaInFormPOST
  154. from content import extractTextFieldsInPOST
  155. from media import removeMetaData
  156. from cache import storePersonInCache
  157. from cache import getPersonFromCache
  158. from httpsig import verifyPostHeaders
  159. from theme import setTheme
  160. import os
  161. import sys
  162. # maximum number of posts to list in outbox feed
  163. maxPostsInFeed=12
  164. # reduced posts for media feed because it can take a while
  165. maxPostsInMediaFeed=6
  166. # number of follows/followers per page
  167. followsPerPage=12
  168. # number of item shares per page
  169. sharesPerPage=12
  170. def readFollowList(filename: str) -> None:
  171. """Returns a list of ActivityPub addresses to follow
  172. """
  173. followlist=[]
  174. if not os.path.isfile(filename):
  175. return followlist
  176. followUsers = open(filename, "r")
  177. for u in followUsers:
  178. if u not in followlist:
  179. nickname,domain = parseHandle(u)
  180. if nickname:
  181. followlist.append(nickname+'@'+domain)
  182. followUsers.close()
  183. return followlist
  184. class PubServer(BaseHTTPRequestHandler):
  185. protocol_version = 'HTTP/1.1'
  186. def _sendReplyToQuestion(self,nickname: str,messageId: str,answer: str) -> None:
  187. """Sends a reply to a question
  188. """
  189. votesFilename= \
  190. self.server.baseDir+'/accounts/'+ \
  191. nickname+'@'+self.server.domain+'/questions.txt'
  192. if os.path.isfile(votesFilename):
  193. # have we already voted on this?
  194. if messageId in open(votesFilename).read():
  195. print('Already voted on message '+messageId)
  196. return
  197. print('Voting on message '+messageId)
  198. print('Vote for: '+answer)
  199. messageJson= \
  200. createPublicPost(self.server.baseDir, \
  201. nickname, \
  202. self.server.domain,self.server.port, \
  203. self.server.httpPrefix, \
  204. answer,False,False,False, \
  205. None,None,None,True, \
  206. messageId,messageId,None, \
  207. None,None,None)
  208. if messageJson:
  209. self.postToNickname=nickname
  210. if self._postToOutbox(messageJson,__version__):
  211. postFilename= \
  212. locatePost(self.server.baseDir,nickname, \
  213. self.server.domain,messageId)
  214. if postFilename:
  215. postJsonObject=loadJson(postFilename)
  216. if postJsonObject:
  217. populateReplies(self.server.baseDir, \
  218. self.server.httpPrefix, \
  219. self.server.domainFull, \
  220. postJsonObject, \
  221. self.server.maxReplies, \
  222. self.server.debug)
  223. # record the vote
  224. votesFile=open(votesFilename,'a+')
  225. if votesFile:
  226. votesFile.write(messageId+'\n')
  227. votesFile.close()
  228. # ensure that the cached post is removed if it exists,
  229. # so that it then will be recreated
  230. cachedPostFilename= \
  231. getCachedPostFilename(self.server.baseDir, \
  232. nickname, \
  233. self.server.domain, \
  234. postJsonObject)
  235. if cachedPostFilename:
  236. if os.path.isfile(cachedPostFilename):
  237. os.remove(cachedPostFilename)
  238. # remove from memory cache
  239. removePostFromCache(postJsonObject, \
  240. self.server.recentPostsCache)
  241. else:
  242. print('ERROR: unable to post vote to outbox')
  243. else:
  244. print('ERROR: unable to create vote')
  245. def _removePostInteractions(self,postJsonObject: {}) -> None:
  246. """Removes potentially sensitive interactions from a post
  247. This is the type of thing which would be of interest to marketers
  248. or of saleable value to them. eg. Knowing who likes who or what.
  249. """
  250. if postJsonObject.get('likes'):
  251. postJsonObject['likes']={'items': []}
  252. if postJsonObject.get('shares'):
  253. postJsonObject['shares']={}
  254. if postJsonObject.get('replies'):
  255. postJsonObject['replies']={}
  256. if postJsonObject.get('bookmarks'):
  257. postJsonObject['bookmarks']={}
  258. if not postJsonObject.get('object'):
  259. return
  260. if not isinstance(postJsonObject['object'], dict):
  261. return
  262. if postJsonObject['object'].get('likes'):
  263. postJsonObject['object']['likes']={'items': []}
  264. if postJsonObject['object'].get('shares'):
  265. postJsonObject['object']['shares']={}
  266. if postJsonObject['object'].get('replies'):
  267. postJsonObject['object']['replies']={}
  268. if postJsonObject['object'].get('bookmarks'):
  269. postJsonObject['object']['bookmarks']={}
  270. def _requestHTTP(self) -> bool:
  271. """Should a http response be given?
  272. """
  273. if not self.headers.get('Accept'):
  274. return False
  275. if self.server.debug:
  276. print('ACCEPT: '+self.headers['Accept'])
  277. if 'image/' in self.headers['Accept']:
  278. return False
  279. if 'video/' in self.headers['Accept']:
  280. return False
  281. if 'audio/' in self.headers['Accept']:
  282. return False
  283. if self.headers['Accept'].startswith('*'):
  284. return False
  285. if 'json' in self.headers['Accept']:
  286. return False
  287. return True
  288. def _fetchAuthenticated(self) -> bool:
  289. """http authentication of GET requests for json
  290. """
  291. if not self.server.authenticatedFetch:
  292. return True
  293. # check that the headers are signed
  294. if not self.headers.get('signature'):
  295. if self.server.debug:
  296. print('WARN: authenticated fetch, GET has no signature in headers')
  297. return False
  298. # get the keyId
  299. keyId=None
  300. signatureParams=self.headers['signature'].split(',')
  301. for signatureItem in signatureParams:
  302. if signatureItem.startswith('keyId='):
  303. if '"' in signatureItem:
  304. keyId=signatureItem.split('"')[1]
  305. break
  306. if not keyId:
  307. if self.server.debug:
  308. print('WARN: authenticated fetch, failed to obtain keyId from signature')
  309. return False
  310. # is the keyId (actor) valid?
  311. if not urlPermitted(keyId,self.server.federationList,"inbox:read"):
  312. if self.server.debug:
  313. print('Authorized fetch failed: '+keyId+' is not permitted')
  314. return False
  315. # make sure we have a session
  316. if not self.server.session:
  317. if self.server.debug:
  318. print('DEBUG: creating new session during authenticated fetch')
  319. self.server.session= \
  320. createSession(self.server.useTor)
  321. # obtain the public key
  322. pubKey= \
  323. getPersonPubKey(self.server.baseDir,self.server.session,keyId, \
  324. self.server.personCache,self.server.debug, \
  325. __version__,self.server.httpPrefix, \
  326. self.server.domain)
  327. if not pubKey:
  328. if self.server.debug:
  329. print('DEBUG: Authenticated fetch failed to obtain public key for '+ \
  330. keyId)
  331. return False
  332. # it is assumed that there will be no message body on authenticated fetches
  333. # and also consequently no digest
  334. GETrequestBody=''
  335. GETrequestDigest=None
  336. # verify the GET request without any digest
  337. if verifyPostHeaders(self.server.httpPrefix, \
  338. pubKey,self.headers, \
  339. self.path,True, \
  340. GETrequestDigest, \
  341. GETrequestBody,debug):
  342. return True
  343. return False
  344. def _login_headers(self,fileFormat: str,length: int) -> None:
  345. self.send_response(200)
  346. self.send_header('Content-type', fileFormat)
  347. self.send_header('Content-Length', str(length))
  348. self.send_header('Host', self.server.domainFull)
  349. self.send_header('WWW-Authenticate', \
  350. 'title="Login to Epicyon", Basic realm="epicyon"')
  351. self.send_header('X-Robots-Tag','noindex')
  352. self.end_headers()
  353. def _logout_headers(self,fileFormat: str,length: int) -> None:
  354. self.send_response(200)
  355. self.send_header('Content-type', fileFormat)
  356. self.send_header('Content-Length', str(length))
  357. self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
  358. self.send_header('Host', self.server.domainFull)
  359. self.send_header('WWW-Authenticate', \
  360. 'title="Login to Epicyon", Basic realm="epicyon"')
  361. self.send_header('X-Robots-Tag','noindex')
  362. self.end_headers()
  363. def _set_headers_base(self,fileFormat: str,length: int,cookie: str) -> None:
  364. self.send_response(200)
  365. self.send_header('Content-type', fileFormat)
  366. if length>-1:
  367. self.send_header('Content-Length', str(length))
  368. if cookie:
  369. self.send_header('Cookie', cookie)
  370. self.send_header('Host', self.server.domainFull)
  371. self.send_header('InstanceID', self.server.instanceId)
  372. self.send_header('X-Robots-Tag','noindex')
  373. self.send_header('Cache-Control','public, max-age=0')
  374. self.send_header('X-Clacks-Overhead','GNU Natalie Nguyen')
  375. self.send_header('Accept-Ranges','none')
  376. def _set_headers(self,fileFormat: str,length: int,cookie: str) -> None:
  377. self._set_headers_base(fileFormat,length,cookie)
  378. self.end_headers()
  379. def _set_headers_head(self,fileFormat: str,length: int,etag: str) -> None:
  380. self._set_headers_base(fileFormat,length,None)
  381. if etag:
  382. self.send_header('ETag',etag)
  383. self.end_headers()
  384. def _set_headers_etag(self,mediaFilename: str,fileFormat: str, \
  385. data,cookie: str) -> None:
  386. self._set_headers_base(fileFormat,len(data),cookie)
  387. etag=None
  388. if os.path.isfile(mediaFilename+'.etag'):
  389. try:
  390. with open(mediaFilename+'.etag', 'r') as etagFile:
  391. etag = etagFile.read()
  392. except:
  393. pass
  394. if not etag:
  395. etag=sha1(data).hexdigest()
  396. try:
  397. with open(mediaFilename+'.etag', 'w') as etagFile:
  398. etagFile.write(etag)
  399. except:
  400. pass
  401. if etag:
  402. self.send_header('ETag',etag)
  403. self.end_headers()
  404. def _redirect_headers(self,redirect: str,cookie: str) -> None:
  405. self.send_response(303)
  406. #self.send_header('Content-type', 'text/html')
  407. if cookie:
  408. self.send_header('Cookie', cookie)
  409. if '://' not in redirect:
  410. print('REDIRECT ERROR: redirect is not an absolute url '+redirect)
  411. self.send_header('Location', redirect)
  412. self.send_header('Host', self.server.domainFull)
  413. self.send_header('InstanceID', self.server.instanceId)
  414. self.send_header('Content-Length', '0')
  415. self.send_header('X-Robots-Tag','noindex')
  416. self.end_headers()
  417. def _httpReturnCode(self,httpCode: int,httpDescription: str) -> None:
  418. msg="<html><head></head><body><h1>"+str(httpCode)+" "+httpDescription+"</h1></body></html>"
  419. msg=msg.encode('utf-8')
  420. self.send_response(httpCode)
  421. self.send_header('Content-Type', 'text/html; charset=utf-8')
  422. self.send_header('Content-Length', str(len(msg)))
  423. self.send_header('X-Robots-Tag','noindex')
  424. self.end_headers()
  425. try:
  426. self.wfile.write(msg)
  427. except Exception as e:
  428. print('Error when showing '+str(httpCode))
  429. print(e)
  430. def _404(self) -> None:
  431. self._httpReturnCode(404,'Not Found')
  432. def _304(self) -> None:
  433. self._httpReturnCode(304,'Resource has not changed')
  434. def _400(self) -> None:
  435. self._httpReturnCode(400,'Bad Request')
  436. def _503(self) -> None:
  437. self._httpReturnCode(503,'Service Unavailable')
  438. def _write(self,msg) -> None:
  439. tries=0
  440. while tries<5:
  441. try:
  442. self.wfile.write(msg)
  443. break
  444. except Exception as e:
  445. print(e)
  446. time.sleep(1)
  447. tries+=1
  448. def _robotsTxt(self) -> bool:
  449. if not self.path.lower().startswith('/robot'):
  450. return False
  451. msg='User-agent: *\nDisallow: /'
  452. msg=msg.encode('utf-8')
  453. self._set_headers('text/plain; charset=utf-8',len(msg),None)
  454. self._write(msg)
  455. return True
  456. def _mastoApi(self) -> bool:
  457. """This is a vestigil mastodon API for the purpose
  458. of returning an empty result to sites like
  459. https://mastopeek.app-dist.eu
  460. """
  461. if not self.path.startswith('/api/v1/'):
  462. return False
  463. if self.server.debug:
  464. print('DEBUG: mastodon api '+self.path)
  465. if self.path=='/api/v1/instance':
  466. adminNickname=getConfigParam(self.server.baseDir,'admin')
  467. instanceDescriptionShort=getConfigParam(self.server.baseDir,'instanceDescriptionShort')
  468. instanceDescription=getConfigParam(self.server.baseDir,'instanceDescription')
  469. instanceTitle=getConfigParam(self.server.baseDir,'instanceTitle')
  470. instanceJson= \
  471. metaDataInstance(instanceTitle, \
  472. instanceDescriptionShort, \
  473. instanceDescription, \
  474. self.server.httpPrefix, \
  475. self.server.baseDir, \
  476. adminNickname, \
  477. self.server.domain,self.server.domainFull, \
  478. self.server.registration, \
  479. self.server.systemLanguage, \
  480. self.server.projectVersion)
  481. msg=json.dumps(instanceJson).encode('utf-8')
  482. if self.headers.get('Accept'):
  483. if 'application/ld+json' in self.headers['Accept']:
  484. self._set_headers('application/ld+json',len(msg),None)
  485. else:
  486. self._set_headers('application/json',len(msg),None)
  487. else:
  488. self._set_headers('application/ld+json',len(msg),None)
  489. self._write(msg)
  490. print('instance metadata sent')
  491. return True
  492. if self.path.startswith('/api/v1/instance/peers'):
  493. # This is just a dummy result.
  494. # Showing the full list of peers would have privacy implications.
  495. # On a large instance you are somewhat lost in the crowd, but on small
  496. # instances a full list of peers would convey a lot of information about
  497. # the interests of a small number of accounts
  498. msg=json.dumps(['mastodon.social',self.server.domainFull]).encode('utf-8')
  499. if self.headers.get('Accept'):
  500. if 'application/ld+json' in self.headers['Accept']:
  501. self._set_headers('application/ld+json',len(msg),None)
  502. else:
  503. self._set_headers('application/json',len(msg),None)
  504. else:
  505. self._set_headers('application/ld+json',len(msg),None)
  506. self._write(msg)
  507. print('instance peers metadata sent')
  508. return True
  509. if self.path.startswith('/api/v1/instance/activity'):
  510. # This is just a dummy result.
  511. msg=json.dumps([]).encode('utf-8')
  512. if self.headers.get('Accept'):
  513. if 'application/ld+json' in self.headers['Accept']:
  514. self._set_headers('application/ld+json',len(msg),None)
  515. else:
  516. self._set_headers('application/json',len(msg),None)
  517. else:
  518. self._set_headers('application/ld+json',len(msg),None)
  519. self._write(msg)
  520. print('instance activity metadata sent')
  521. return True
  522. self._404()
  523. return True
  524. def _nodeinfo(self) -> bool:
  525. if not self.path.startswith('/nodeinfo/2.0'):
  526. return False
  527. if self.server.debug:
  528. print('DEBUG: nodeinfo '+self.path)
  529. info=metaDataNodeInfo(self.server.baseDir,self.server.registration,self.server.projectVersion)
  530. if info:
  531. msg=json.dumps(info).encode('utf-8')
  532. if self.headers.get('Accept'):
  533. if 'application/ld+json' in self.headers['Accept']:
  534. self._set_headers('application/ld+json',len(msg),None)
  535. else:
  536. self._set_headers('application/json',len(msg),None)
  537. else:
  538. self._set_headers('application/ld+json',len(msg),None)
  539. self._write(msg)
  540. print('nodeinfo sent')
  541. return True
  542. self._404()
  543. return True
  544. def _webfinger(self) -> bool:
  545. if not self.path.startswith('/.well-known'):
  546. return False
  547. if self.server.debug:
  548. print('DEBUG: WEBFINGER well-known')
  549. if self.server.debug:
  550. print('DEBUG: WEBFINGER host-meta')
  551. if self.path.startswith('/.well-known/host-meta'):
  552. wfResult=webfingerMeta(self.server.httpPrefix,self.server.domainFull)
  553. if wfResult:
  554. msg=wfResult.encode('utf-8')
  555. self._set_headers('application/xrd+xml',len(msg),None)
  556. self._write(msg)
  557. return True
  558. self._404()
  559. return True
  560. if self.path.startswith('/.well-known/nodeinfo'):
  561. wfResult=webfingerNodeInfo(self.server.httpPrefix,self.server.domainFull)
  562. if wfResult:
  563. msg=json.dumps(wfResult).encode('utf-8')
  564. if self.headers.get('Accept'):
  565. if 'application/ld+json' in self.headers['Accept']:
  566. self._set_headers('application/ld+json',len(msg),None)
  567. else:
  568. self._set_headers('application/json',len(msg),None)
  569. else:
  570. self._set_headers('application/ld+json',len(msg),None)
  571. self._write(msg)
  572. return True
  573. self._404()
  574. return True
  575. if self.server.debug:
  576. print('DEBUG: WEBFINGER lookup '+self.path+' '+str(self.server.baseDir))
  577. wfResult=webfingerLookup(self.path,self.server.baseDir,self.server.port,self.server.debug)
  578. if wfResult:
  579. msg=json.dumps(wfResult).encode('utf-8')
  580. self._set_headers('application/jrd+json',len(msg),None)
  581. self._write(msg)
  582. else:
  583. if self.server.debug:
  584. print('DEBUG: WEBFINGER lookup 404 '+self.path)
  585. self._404()
  586. return True
  587. def _permittedDir(self,path: str) -> bool:
  588. """These are special paths which should not be accessible
  589. directly via GET or POST
  590. """
  591. if path.startswith('/wfendpoints') or \
  592. path.startswith('/keys') or \
  593. path.startswith('/accounts'):
  594. return False
  595. return True
  596. def _postToOutbox(self,messageJson: {},version: str) -> bool:
  597. """post is received by the outbox
  598. Client to server message post
  599. https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
  600. """
  601. if not messageJson.get('type'):
  602. if self.server.debug:
  603. print('DEBUG: POST to outbox has no "type" parameter')
  604. return False
  605. if not messageJson.get('object') and messageJson.get('content'):
  606. if messageJson['type']!='Create':
  607. # https://www.w3.org/TR/activitypub/#object-without-create
  608. if self.server.debug:
  609. print('DEBUG: POST to outbox - adding Create wrapper')
  610. messageJson= \
  611. outboxMessageCreateWrap(self.server.httpPrefix, \
  612. self.postToNickname, \
  613. self.server.domain, \
  614. self.server.port, \
  615. messageJson)
  616. if messageJson['type']=='Create':
  617. if not (messageJson.get('id') and \
  618. messageJson.get('type') and \
  619. messageJson.get('actor') and \
  620. messageJson.get('object') and \
  621. messageJson.get('to')):
  622. if self.server.debug:
  623. print('DEBUG: POST to outbox - Create does not have the required parameters')
  624. return False
  625. testDomain,testPort=getDomainFromActor(messageJson['actor'])
  626. if testPort:
  627. if testPort!=80 and testPort!=443:
  628. testDomain=testDomain+':'+str(testPort)
  629. if isBlockedDomain(self.server.baseDir,testDomain):
  630. if self.server.debug:
  631. print('DEBUG: domain is blocked: '+messageJson['actor'])
  632. return False
  633. # https://www.w3.org/TR/activitypub/#create-activity-outbox
  634. messageJson['object']['attributedTo']=messageJson['actor']
  635. if messageJson['object'].get('attachment'):
  636. attachmentIndex=0
  637. if messageJson['object']['attachment'][attachmentIndex].get('mediaType'):
  638. fileExtension='png'
  639. mediaTypeStr= \
  640. messageJson['object']['attachment'][attachmentIndex]['mediaType']
  641. if mediaTypeStr.endswith('jpeg'):
  642. fileExtension='jpg'
  643. elif mediaTypeStr.endswith('gif'):
  644. fileExtension='gif'
  645. elif mediaTypeStr.endswith('webp'):
  646. fileExtension='webp'
  647. elif mediaTypeStr.endswith('audio/mpeg'):
  648. fileExtension='mp3'
  649. elif mediaTypeStr.endswith('ogg'):
  650. fileExtension='ogg'
  651. elif mediaTypeStr.endswith('mp4'):
  652. fileExtension='mp4'
  653. elif mediaTypeStr.endswith('webm'):
  654. fileExtension='webm'
  655. elif mediaTypeStr.endswith('ogv'):
  656. fileExtension='ogv'
  657. mediaDir= \
  658. self.server.baseDir+'/accounts/'+ \
  659. self.postToNickname+'@'+self.server.domain
  660. uploadMediaFilename=mediaDir+'/upload.'+fileExtension
  661. if not os.path.isfile(uploadMediaFilename):
  662. del messageJson['object']['attachment']
  663. else:
  664. # generate a path for the uploaded image
  665. mPath=getMediaPath()
  666. mediaPath=mPath+'/'+createPassword(32)+'.'+fileExtension
  667. createMediaDirs(self.server.baseDir,mPath)
  668. mediaFilename=self.server.baseDir+'/'+mediaPath
  669. # move the uploaded image to its new path
  670. os.rename(uploadMediaFilename,mediaFilename)
  671. # change the url of the attachment
  672. messageJson['object']['attachment'][attachmentIndex]['url']= \
  673. self.server.httpPrefix+'://'+self.server.domainFull+ \
  674. '/'+mediaPath
  675. permittedOutboxTypes=[
  676. 'Create','Announce','Like','Follow','Undo', \
  677. 'Update','Add','Remove','Block','Delete', \
  678. 'Delegate','Skill','Bookmark'
  679. ]
  680. if messageJson['type'] not in permittedOutboxTypes:
  681. if self.server.debug:
  682. print('DEBUG: POST to outbox - '+messageJson['type']+ \
  683. ' is not a permitted activity type')
  684. return False
  685. if messageJson.get('id'):
  686. postId=messageJson['id'].replace('/activity','').replace('/undo','')
  687. if self.server.debug:
  688. print('DEBUG: id attribute exists within POST to outbox')
  689. else:
  690. if self.server.debug:
  691. print('DEBUG: No id attribute within POST to outbox')
  692. postId=None
  693. if self.server.debug:
  694. print('DEBUG: savePostToBox')
  695. if messageJson['type']!='Upgrade':
  696. savedFilename= \
  697. savePostToBox(self.server.baseDir, \
  698. self.server.httpPrefix, \
  699. postId, \
  700. self.postToNickname, \
  701. self.server.domainFull,messageJson,'outbox')
  702. if messageJson['type']=='Create' or \
  703. messageJson['type']=='Question' or \
  704. messageJson['type']=='Note' or \
  705. messageJson['type']=='Announce':
  706. inboxUpdateIndex('outbox',self.server.baseDir, \
  707. self.postToNickname+'@'+self.server.domain, \
  708. savedFilename,self.server.debug)
  709. if outboxAnnounce(self.server.recentPostsCache, \
  710. self.server.baseDir,messageJson,self.server.debug):
  711. if self.server.debug:
  712. print('DEBUG: Updated announcements (shares) collection for the post associated with the Announce activity')
  713. if not self.server.session:
  714. if self.server.debug:
  715. print('DEBUG: creating new session for c2s')
  716. self.server.session= \
  717. createSession(self.server.useTor)
  718. if self.server.debug:
  719. print('DEBUG: sending c2s post to followers')
  720. # remove inactive threads
  721. inactiveFollowerThreads=[]
  722. for th in self.server.followersThreads:
  723. if not th.is_alive():
  724. inactiveFollowerThreads.append(th)
  725. for th in inactiveFollowerThreads:
  726. self.server.followersThreads.remove(th)
  727. if self.server.debug:
  728. print('DEBUG: '+str(len(self.server.followersThreads))+' followers threads active')
  729. # retain up to 20 threads
  730. if len(self.server.followersThreads)>20:
  731. # kill the thread if it is still alive
  732. if self.server.followersThreads[0].is_alive():
  733. self.server.followersThreads[0].kill()
  734. # remove it from the list
  735. self.server.followersThreads.pop(0)
  736. # create a thread to send the post to followers
  737. followersThread= \
  738. sendToFollowersThread(self.server.session, \
  739. self.server.baseDir, \
  740. self.postToNickname, \
  741. self.server.domain, \
  742. self.server.port, \
  743. self.server.httpPrefix, \
  744. self.server.federationList, \
  745. self.server.sendThreads, \
  746. self.server.postLog, \
  747. self.server.cachedWebfingers, \
  748. self.server.personCache, \
  749. messageJson,self.server.debug, \
  750. self.server.projectVersion)
  751. self.server.followersThreads.append(followersThread)
  752. if self.server.debug:
  753. print('DEBUG: handle any unfollow requests')
  754. outboxUndoFollow(self.server.baseDir,messageJson,self.server.debug)
  755. if self.server.debug:
  756. print('DEBUG: handle delegation requests')
  757. outboxDelegate(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
  758. if self.server.debug:
  759. print('DEBUG: handle skills changes requests')
  760. outboxSkills(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
  761. if self.server.debug:
  762. print('DEBUG: handle availability changes requests')
  763. outboxAvailability(self.server.baseDir,self.postToNickname,messageJson,self.server.debug)
  764. if self.server.debug:
  765. print('DEBUG: handle any like requests')
  766. outboxLike(self.server.recentPostsCache, \
  767. self.server.baseDir,self.server.httpPrefix, \
  768. self.postToNickname,self.server.domain,self.server.port, \
  769. messageJson,self.server.debug)
  770. if self.server.debug:
  771. print('DEBUG: handle any undo like requests')
  772. outboxUndoLike(self.server.baseDir,self.server.httpPrefix, \
  773. self.postToNickname,self.server.domain,self.server.port, \
  774. messageJson,self.server.debug)
  775. if self.server.debug:
  776. print('DEBUG: handle any bookmark requests')
  777. outboxBookmark(self.server.recentPostsCache, \
  778. self.server.baseDir,self.server.httpPrefix, \
  779. self.postToNickname,self.server.domain,self.server.port, \
  780. messageJson,self.server.debug)
  781. if self.server.debug:
  782. print('DEBUG: handle any undo bookmark requests')
  783. outboxUndoBookmark(self.server.recentPostsCache, \
  784. self.server.baseDir,self.server.httpPrefix, \
  785. self.postToNickname,self.server.domain,self.server.port, \
  786. messageJson,self.server.debug)
  787. if self.server.debug:
  788. print('DEBUG: handle delete requests')
  789. outboxDelete(self.server.baseDir,self.server.httpPrefix, \
  790. self.postToNickname,self.server.domain, \
  791. messageJson,self.server.debug, \
  792. self.server.allowDeletion)
  793. if self.server.debug:
  794. print('DEBUG: handle block requests')
  795. outboxBlock(self.server.baseDir,self.server.httpPrefix, \
  796. self.postToNickname,self.server.domain, \
  797. self.server.port,
  798. messageJson,self.server.debug)
  799. if self.server.debug:
  800. print('DEBUG: handle undo block requests')
  801. outboxUndoBlock(self.server.baseDir,self.server.httpPrefix, \
  802. self.postToNickname,self.server.domain, \
  803. self.server.port,
  804. messageJson,self.server.debug)
  805. if self.server.debug:
  806. print('DEBUG: handle share uploads')
  807. outboxShareUpload(self.server.baseDir,self.server.httpPrefix, \
  808. self.postToNickname,self.server.domain, \
  809. self.server.port,
  810. messageJson,self.server.debug)
  811. if self.server.debug:
  812. print('DEBUG: handle undo share uploads')
  813. outboxUndoShareUpload(self.server.baseDir,self.server.httpPrefix, \
  814. self.postToNickname,self.server.domain, \
  815. self.server.port,
  816. messageJson,self.server.debug)
  817. if self.server.debug:
  818. print('DEBUG: sending c2s post to named addresses')
  819. print('c2s sender: '+self.postToNickname+'@'+ \
  820. self.server.domain+':'+str(self.server.port))
  821. sendToNamedAddresses(self.server.session,self.server.baseDir, \
  822. self.postToNickname,self.server.domain, \
  823. self.server.port, \
  824. self.server.httpPrefix, \
  825. self.server.federationList, \
  826. self.server.sendThreads, \
  827. self.server.postLog, \
  828. self.server.cachedWebfingers, \
  829. self.server.personCache, \
  830. messageJson,self.server.debug, \
  831. self.server.projectVersion)
  832. return True
  833. def _postToOutboxThread(self,messageJson: {}) -> bool:
  834. """Creates a thread to send a post
  835. """
  836. accountOutboxThreadName=self.postToNickname
  837. if not accountOutboxThreadName:
  838. accountOutboxThreadName='*'
  839. if self.server.outboxThread.get(accountOutboxThreadName):
  840. print('Waiting for previous outbox thread to end')
  841. waitCtr=0
  842. while self.server.outboxThread[accountOutboxThreadName].isAlive() and waitCtr<8:
  843. time.sleep(1)
  844. waitCtr+=1
  845. if waitCtr>=8:
  846. self.server.outboxThread[accountOutboxThreadName].kill()
  847. print('Creating outbox thread')
  848. self.server.outboxThread[accountOutboxThreadName]= \
  849. threadWithTrace(target=self._postToOutbox, \
  850. args=(messageJson.copy(),__version__),daemon=True)
  851. print('Starting outbox thread')
  852. self.server.outboxThread[accountOutboxThreadName].start()
  853. return True
  854. def _inboxQueueCleardown(self) -> None:
  855. """ Check if the queue is full and remove oldest items if it is
  856. """
  857. if len(self.server.inboxQueue)>=self.server.maxQueueLength:
  858. print('Inbox queue is full ('+str(self.server.maxQueueLength)+' items). Removing oldest items.')
  859. cleardownStartTime=time.time()
  860. while len(self.server.inboxQueue) >= self.server.maxQueueLength-4:
  861. queueFilename=self.server.inboxQueue[0]
  862. if os.path.isfile(queueFilename):
  863. try:
  864. os.remove(queueFilename)
  865. except:
  866. pass
  867. self.server.inboxQueue.pop(0)
  868. timeDiff=str(int((time.time()-cleardownStartTime)*1000))
  869. print('Inbox cleardown took '+timeDiff+' mS')
  870. def _updateInboxQueue(self,nickname: str,messageJson: {}, \
  871. messageBytes: str) -> int:
  872. """Update the inbox queue
  873. """
  874. self._inboxQueueCleardown()
  875. # Convert the headers needed for signature verification to dict
  876. headersDict={}
  877. headersDict['host']=self.headers['host']
  878. headersDict['signature']=self.headers['signature']
  879. if self.headers.get('Date'):
  880. headersDict['Date']=self.headers['Date']
  881. if self.headers.get('digest'):
  882. headersDict['digest']=self.headers['digest']
  883. if self.headers.get('Content-type'):
  884. headersDict['Content-type']=self.headers['Content-type']
  885. if self.headers.get('Content-Length'):
  886. headersDict['Content-Length']=self.headers['Content-Length']
  887. elif self.headers.get('content-length'):
  888. headersDict['content-length']=self.headers['content-length']
  889. # For follow activities add a 'to' field, which is a copy
  890. # of the object field
  891. messageJson,toFieldExists= \
  892. addToField('Follow',messageJson,self.server.debug)
  893. # For like activities add a 'to' field, which is a copy of
  894. # the actor within the object field
  895. messageJson,toFieldExists= \
  896. addToField('Like',messageJson,self.server.debug)
  897. beginSaveTime=time.time()
  898. # save the json for later queue processing
  899. queueFilename = \
  900. savePostToInboxQueue(self.server.baseDir,
  901. self.server.httpPrefix,
  902. nickname,
  903. self.server.domainFull,
  904. messageJson,
  905. messageBytes.decode('utf-8'),
  906. headersDict,
  907. self.path,
  908. self.server.debug)
  909. if queueFilename:
  910. # add json to the queue
  911. if queueFilename not in self.server.inboxQueue:
  912. self.server.inboxQueue.append(queueFilename)
  913. if self.server.debug:
  914. timeDiff=int((time.time()-beginSaveTime)*1000)
  915. if timeDiff>200:
  916. print('SLOW: slow save of inbox queue item '+queueFilename+' took '+str(timeDiff)+' mS')
  917. self.send_response(201)
  918. self.end_headers()
  919. self.server.POSTbusy=False
  920. return 0
  921. return 2
  922. def _isAuthorized(self) -> bool:
  923. if self.path.startswith('/icons/') or \
  924. self.path.startswith('/avatars/') or \
  925. self.path.startswith('/favicon.ico'):
  926. return False
  927. # token based authenticated used by the web interface
  928. if self.headers.get('Cookie'):
  929. if self.headers['Cookie'].startswith('epicyon='):
  930. tokenStr=self.headers['Cookie'].split('=',1)[1].strip()
  931. if ';' in tokenStr:
  932. tokenStr=tokenStr.split(';')[0].strip()
  933. if self.server.tokensLookup.get(tokenStr):
  934. nickname=self.server.tokensLookup[tokenStr]
  935. # default to the inbox of the person
  936. if self.path=='/':
  937. self.path='/users/'+nickname+'/inbox'
  938. # check that the path contains the same nickname as the cookie
  939. # otherwise it would be possible to be authorized to use
  940. # an account you don't own
  941. if '/'+nickname+'/' in self.path:
  942. return True
  943. if self.path.endswith('/'+nickname):
  944. return True
  945. print('AUTH: nickname '+nickname+' was not found in path '+self.path)
  946. return False
  947. if self.server.debug:
  948. print('AUTH: epicyon cookie authorization failed, header='+self.headers['Cookie'].replace('epicyon=','')+' tokenStr='+tokenStr+' tokens='+str(self.server.tokensLookup))
  949. return False
  950. print('AUTH: Header cookie was not authorized')
  951. return False
  952. # basic auth
  953. if self.headers.get('Authorization'):
  954. if authorize(self.server.baseDir,self.path, \
  955. self.headers['Authorization'], \
  956. self.server.debug):
  957. return True
  958. print('AUTH: Basic auth did not authorize '+self.headers['Authorization'])
  959. return False
  960. def _clearLoginDetails(self,nickname: str):
  961. """Clears login details for the given account
  962. """
  963. # remove any token
  964. if self.server.tokens.get(nickname):
  965. del self.server.tokensLookup[self.server.tokens[nickname]]
  966. del self.server.tokens[nickname]
  967. self.send_response(303)
  968. self.send_header('Content-Length', '0')
  969. self.send_header('Set-Cookie', 'epicyon=; SameSite=Strict')
  970. self.send_header('Location', \
  971. self.server.httpPrefix+'://'+ \
  972. self.server.domainFull+'/login')
  973. self.send_header('X-Robots-Tag','noindex')
  974. self.end_headers()
  975. def _benchmarkGETtimings(self,GETstartTime,GETtimings: [],getID: int):
  976. """Updates a list containing how long each segment of GET takes
  977. """
  978. if self.server.debug:
  979. timeDiff=int((time.time()-GETstartTime)*1000)
  980. logEvent=False
  981. if timeDiff>100:
  982. logEvent=True
  983. if GETtimings:
  984. timeDiff=int(timeDiff-int(GETtimings[-1]))
  985. GETtimings.append(str(timeDiff))
  986. if logEvent:
  987. ctr=1
  988. for timeDiff in GETtimings:
  989. print('GET TIMING|'+str(ctr)+'|'+timeDiff)
  990. ctr+=1
  991. def _benchmarkPOSTtimings(self,POSTstartTime,POSTtimings: [],postID: int):
  992. """Updates a list containing how long each segment of POST takes
  993. """
  994. if self.server.debug:
  995. timeDiff=int((time.time()-POSTstartTime)*1000)
  996. logEvent=False
  997. if timeDiff>100:
  998. logEvent=True
  999. if POSTtimings:
  1000. timeDiff=int(timeDiff-int(POSTtimings[-1]))
  1001. POSTtimings.append(str(timeDiff))
  1002. if logEvent:
  1003. ctr=1
  1004. for timeDiff in POSTtimings:
  1005. print('POST TIMING|'+str(ctr)+'|'+timeDiff)
  1006. ctr+=1
  1007. def do_GET(self):
  1008. GETstartTime=time.time()
  1009. GETtimings=[]
  1010. # Since fediverse crawlers are quite active, make returning info to them high priority
  1011. # get nodeinfo endpoint
  1012. if self._nodeinfo():
  1013. return
  1014. self._benchmarkGETtimings(GETstartTime,GETtimings,1)
  1015. # minimal mastodon api
  1016. if self._mastoApi():
  1017. return
  1018. self._benchmarkGETtimings(GETstartTime,GETtimings,2)
  1019. if self.path=='/logout':
  1020. msg=htmlLogin(self.server.translate, \
  1021. self.server.baseDir,False).encode('utf-8')
  1022. self._logout_headers('text/html',len(msg))
  1023. self._write(msg)
  1024. return
  1025. self._benchmarkGETtimings(GETstartTime,GETtimings,3)
  1026. # replace https://domain/@nick with https://domain/users/nick
  1027. if self.path.startswith('/@'):
  1028. self.path=self.path.replace('/@','/users/')
  1029. # redirect music to #nowplaying list
  1030. if self.path=='/music' or self.path=='/nowplaying':
  1031. self.path='/tags/nowplaying'
  1032. if self.server.debug:
  1033. print('DEBUG: GET from '+self.server.baseDir+ \
  1034. ' path: '+self.path+' busy: '+ \
  1035. str(self.server.GETbusy))
  1036. if self.server.debug:
  1037. print(str(self.headers))
  1038. cookie=None
  1039. if self.headers.get('Cookie'):
  1040. cookie=self.headers['Cookie']
  1041. self._benchmarkGETtimings(GETstartTime,GETtimings,4)
  1042. # check authorization
  1043. authorized = self._isAuthorized()
  1044. if authorized:
  1045. if self.server.debug:
  1046. print('GET Authorization granted')
  1047. else:
  1048. if self.server.debug:
  1049. print('GET Not authorized')
  1050. self._benchmarkGETtimings(GETstartTime,GETtimings,5)
  1051. if not self.server.session:
  1052. print('Starting new session')
  1053. self.server.session= \
  1054. createSession(self.server.useTor)
  1055. self._benchmarkGETtimings(GETstartTime,GETtimings,6)
  1056. # is this a html request?
  1057. htmlGET=False
  1058. if self.headers.get('Accept'):
  1059. if self._requestHTTP():
  1060. htmlGET=True
  1061. else:
  1062. self._400()
  1063. return
  1064. self._benchmarkGETtimings(GETstartTime,GETtimings,7)
  1065. # treat shared inbox paths consistently
  1066. if self.path=='/sharedInbox' or \
  1067. self.path=='/users/inbox' or \
  1068. self.path=='/actor/inbox' or \
  1069. self.path=='/users/'+self.server.domain:
  1070. # if shared inbox is not enabled
  1071. if not self.server.enableSharedInbox:
  1072. self._503()
  1073. return
  1074. self.path='/inbox'
  1075. self._benchmarkGETtimings(GETstartTime,GETtimings,8)
  1076. # show the person options screen with view/follow/block/report
  1077. if htmlGET and '/users/' in self.path:
  1078. if '?options=' in self.path:
  1079. optionsStr=self.path.split('?options=')[1]
  1080. originPathStr=self.path.split('?options=')[0]
  1081. if ';' in optionsStr:
  1082. pageNumber=1
  1083. optionsList=optionsStr.split(';')
  1084. optionsActor=optionsList[0]
  1085. optionsPageNumber=optionsList[1]
  1086. optionsProfileUrl=optionsList[2]
  1087. if optionsPageNumber.isdigit():
  1088. pageNumber=int(optionsPageNumber)
  1089. optionsLink=None
  1090. if len(optionsList)>3:
  1091. optionsLink=optionsList[3]
  1092. donateUrl=None
  1093. actorJson=getPersonFromCache(self.server.baseDir,optionsActor,self.server.personCache)
  1094. if actorJson:
  1095. donateUrl=getDonationUrl(actorJson)
  1096. msg=htmlPersonOptions(self.server.translate, \
  1097. self.server.baseDir, \
  1098. self.server.domain, \
  1099. originPathStr, \
  1100. optionsActor, \
  1101. optionsProfileUrl, \
  1102. optionsLink, \
  1103. pageNumber,donateUrl).encode()
  1104. self._set_headers('text/html',len(msg),cookie)
  1105. self._write(msg)
  1106. return
  1107. originPathStrAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+originPathStr
  1108. self._redirect_headers(originPathStrAbsolute,cookie)
  1109. return
  1110. self._benchmarkGETtimings(GETstartTime,GETtimings,9)
  1111. # remove a shared item
  1112. if htmlGET and '?rmshare=' in self.path:
  1113. shareName=self.path.split('?rmshare=')[1]
  1114. shareName=shareName.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').strip()
  1115. actor= \
  1116. self.server.httpPrefix+'://'+self.server.domainFull+ \
  1117. self.path.split('?rmshare=')[0]
  1118. msg=htmlRemoveSharedItem(self.server.translate, \
  1119. self.server.baseDir, \
  1120. actor,shareName).encode()
  1121. if not msg:
  1122. self._redirect_headers(actor+'/tlshares',cookie)
  1123. return
  1124. self._set_headers('text/html',len(msg),cookie)
  1125. self._write(msg)
  1126. return
  1127. self._benchmarkGETtimings(GETstartTime,GETtimings,10)
  1128. if self.path.startswith('/terms'):
  1129. msg=htmlTermsOfService(self.server.baseDir, \
  1130. self.server.httpPrefix, \
  1131. self.server.domainFull).encode()
  1132. self._login_headers('text/html',len(msg))
  1133. self._write(msg)
  1134. return
  1135. self._benchmarkGETtimings(GETstartTime,GETtimings,11)
  1136. if self.path.startswith('/about'):
  1137. msg=htmlAbout(self.server.baseDir, \
  1138. self.server.httpPrefix, \
  1139. self.server.domainFull).encode()
  1140. self._login_headers('text/html',len(msg))
  1141. self._write(msg)
  1142. return
  1143. self._benchmarkGETtimings(GETstartTime,GETtimings,12)
  1144. # send robots.txt if asked
  1145. if self._robotsTxt():
  1146. return
  1147. self._benchmarkGETtimings(GETstartTime,GETtimings,13)
  1148. # if not authorized then show the login screen
  1149. if htmlGET and self.path!='/login' and self.path!='/':
  1150. if '/media/' not in self.path and \
  1151. '/sharefiles/' not in self.path and \
  1152. '/statuses/' not in self.path and \
  1153. '/emoji/' not in self.path and \
  1154. '/tags/' not in self.path and \
  1155. '/avatars/' not in self.path and \
  1156. '/icons/' not in self.path:
  1157. divertToLoginScreen=True
  1158. if self.path.startswith('/users/'):
  1159. nickStr=self.path.split('/users/')[1]
  1160. if '/' not in nickStr and '?' not in nickStr:
  1161. divertToLoginScreen=False
  1162. else:
  1163. if self.path.endswith('/following') or \
  1164. self.path.endswith('/followers') or \
  1165. self.path.endswith('/skills') or \
  1166. self.path.endswith('/roles') or \
  1167. self.path.endswith('/shares'):
  1168. divertToLoginScreen=False
  1169. if divertToLoginScreen and not authorized:
  1170. if self.server.debug:
  1171. print('DEBUG: divertToLoginScreen='+str(divertToLoginScreen))
  1172. print('DEBUG: authorized='+str(authorized))
  1173. print('DEBUG: path='+self.path)
  1174. self.send_response(303)
  1175. self.send_header('Location', \
  1176. self.server.httpPrefix+'://'+ \
  1177. self.server.domainFull+'/login')
  1178. self.send_header('Content-Length', '0')
  1179. self.send_header('X-Robots-Tag','noindex')
  1180. self.end_headers()
  1181. return
  1182. self._benchmarkGETtimings(GETstartTime,GETtimings,14)
  1183. # get css
  1184. # Note that this comes before the busy flag to avoid conflicts
  1185. if self.path.endswith('.css'):
  1186. if os.path.isfile('epicyon-profile.css'):
  1187. tries=0
  1188. while tries<5:
  1189. try:
  1190. with open('epicyon-profile.css', 'r') as cssfile:
  1191. css = cssfile.read()
  1192. break
  1193. except Exception as e:
  1194. print(e)
  1195. time.sleep(1)
  1196. tries+=1
  1197. msg=css.encode('utf-8')
  1198. self._set_headers('text/css',len(msg),cookie)
  1199. self._write(msg)
  1200. return
  1201. self._404()
  1202. return
  1203. self._benchmarkGETtimings(GETstartTime,GETtimings,15)
  1204. # image on login screen
  1205. if self.path=='/login.png' or \
  1206. self.path=='/login.gif' or \
  1207. self.path=='/login.webp' or \
  1208. self.path=='/login.jpeg' or \
  1209. self.path=='/login.jpg':
  1210. mediaFilename= \
  1211. self.server.baseDir+'/accounts'+self.path
  1212. if os.path.isfile(mediaFilename):
  1213. tries=0
  1214. mediaBinary=None
  1215. while tries<5:
  1216. try:
  1217. with open(mediaFilename, 'rb') as avFile:
  1218. mediaBinary = avFile.read()
  1219. break
  1220. except Exception as e:
  1221. print(e)
  1222. time.sleep(1)
  1223. tries+=1
  1224. if mediaBinary:
  1225. self._set_headers('image/png',len(mediaBinary),cookie)
  1226. self._write(mediaBinary)
  1227. return
  1228. self._404()
  1229. return
  1230. self._benchmarkGETtimings(GETstartTime,GETtimings,16)
  1231. # login screen background image
  1232. if self.path=='/login-background.png':
  1233. mediaFilename= \
  1234. self.server.baseDir+'/accounts/login-background.png'
  1235. if os.path.isfile(mediaFilename):
  1236. tries=0
  1237. mediaBinary=None
  1238. while tries<5:
  1239. try:
  1240. with open(mediaFilename, 'rb') as avFile:
  1241. mediaBinary = avFile.read()
  1242. break
  1243. except Exception as e:
  1244. print(e)
  1245. time.sleep(1)
  1246. tries+=1
  1247. if mediaBinary:
  1248. self._set_headers('image/png',len(mediaBinary),cookie)
  1249. self._write(mediaBinary)
  1250. return
  1251. self._404()
  1252. return
  1253. self._benchmarkGETtimings(GETstartTime,GETtimings,17)
  1254. # follow screen background image
  1255. if self.path=='/follow-background.png':
  1256. mediaFilename= \
  1257. self.server.baseDir+'/accounts/follow-background.png'
  1258. if os.path.isfile(mediaFilename):
  1259. tries=0
  1260. mediaBinary=None
  1261. while tries<5:
  1262. try:
  1263. with open(mediaFilename, 'rb') as avFile:
  1264. mediaBinary = avFile.read()
  1265. break
  1266. except Exception as e:
  1267. print(e)
  1268. time.sleep(1)
  1269. tries+=1
  1270. if mediaBinary:
  1271. self._set_headers('image/png',len(mediaBinary),cookie)
  1272. self._write(mediaBinary)
  1273. return
  1274. self._404()
  1275. return
  1276. self._benchmarkGETtimings(GETstartTime,GETtimings,18)
  1277. # emoji images
  1278. if '/emoji/' in self.path:
  1279. if self.path.endswith('.png') or \
  1280. self.path.endswith('.jpg') or \
  1281. self.path.endswith('.gif'):
  1282. emojiStr=self.path.split('/emoji/')[1]
  1283. emojiFilename= \
  1284. self.server.baseDir+'/emoji/'+emojiStr
  1285. if os.path.isfile(emojiFilename):
  1286. mediaImageType='png'
  1287. if emojiFilename.endswith('.png'):
  1288. mediaImageType='png'
  1289. elif emojiFilename.endswith('.jpg'):
  1290. mediaImageType='jpeg'
  1291. elif emojiFilename.endswith('.webp'):
  1292. mediaImageType='webp'
  1293. else:
  1294. mediaImageType='gif'
  1295. with open(emojiFilename, 'rb') as avFile:
  1296. mediaBinary = avFile.read()
  1297. self._set_headers('image/'+mediaImageType,len(mediaBinary),cookie)
  1298. self._write(mediaBinary)
  1299. return
  1300. self._404()
  1301. return
  1302. self._benchmarkGETtimings(GETstartTime,GETtimings,19)
  1303. # show media
  1304. # Note that this comes before the busy flag to avoid conflicts
  1305. if '/media/' in self.path:
  1306. if self.path.endswith('.png') or \
  1307. self.path.endswith('.jpg') or \
  1308. self.path.endswith('.gif') or \
  1309. self.path.endswith('.webp') or \
  1310. self.path.endswith('.mp4') or \
  1311. self.path.endswith('.ogv') or \
  1312. self.path.endswith('.mp3') or \
  1313. self.path.endswith('.ogg'):
  1314. mediaStr=self.path.split('/media/')[1]
  1315. mediaFilename= \
  1316. self.server.baseDir+'/media/'+mediaStr
  1317. if os.path.isfile(mediaFilename):
  1318. mediaFileType='image/png'
  1319. if mediaFilename.endswith('.png'):
  1320. mediaFileType='image/png'
  1321. elif mediaFilename.endswith('.jpg'):
  1322. mediaFileType='image/jpeg'
  1323. elif mediaFilename.endswith('.gif'):
  1324. mediaFileType='image/gif'
  1325. elif mediaFilename.endswith('.webp'):
  1326. mediaFileType='image/webp'
  1327. elif mediaFilename.endswith('.mp4'):
  1328. mediaFileType='video/mp4'
  1329. elif mediaFilename.endswith('.ogv'):
  1330. mediaFileType='video/ogv'
  1331. elif mediaFilename.endswith('.mp3'):
  1332. mediaFileType='audio/mpeg'
  1333. elif mediaFilename.endswith('.ogg'):
  1334. mediaFileType='audio/ogg'
  1335. # does an etag header exist?
  1336. etagHeader='If-None-Match'
  1337. if not self.headers.get(etagHeader):
  1338. etagHeader='if-none-match'
  1339. if not self.headers.get(etagHeader):
  1340. etagHeader='If-none-match'
  1341. if self.headers.get(etagHeader):
  1342. oldEtag=self.headers['If-None-Match']
  1343. if os.path.isfile(mediaFilename+'.etag'):
  1344. # load the etag from file
  1345. currEtag=''
  1346. try:
  1347. with open(mediaFilename, 'r') as etagFile:
  1348. currEtag = etagFile.read()
  1349. except:
  1350. pass
  1351. if oldEtag==currEtag:
  1352. # The file has not changed
  1353. self._304()
  1354. return
  1355. with open(mediaFilename, 'rb') as avFile:
  1356. mediaBinary = avFile.read()
  1357. self._set_headers_etag(mediaFilename,mediaFileType,mediaBinary,cookie)
  1358. self._write(mediaBinary)
  1359. return
  1360. self._404()
  1361. return
  1362. self._benchmarkGETtimings(GETstartTime,GETtimings,20)
  1363. # show shared item images
  1364. # Note that this comes before the busy flag to avoid conflicts
  1365. if '/sharefiles/' in self.path:
  1366. if self.path.endswith('.png') or \
  1367. self.path.endswith('.jpg') or \
  1368. self.path.endswith('.webp') or \
  1369. self.path.endswith('.gif'):
  1370. mediaStr=self.path.split('/sharefiles/')[1]
  1371. mediaFilename= \
  1372. self.server.baseDir+'/sharefiles/'+mediaStr
  1373. if os.path.isfile(mediaFilename):
  1374. mediaFileType='png'
  1375. if mediaFilename.endswith('.png'):
  1376. mediaFileType='png'
  1377. elif mediaFilename.endswith('.jpg'):
  1378. mediaFileType='jpeg'
  1379. elif mediaFilename.endswith('.webp'):
  1380. mediaFileType='webp'
  1381. else:
  1382. mediaFileType='gif'
  1383. with open(mediaFilename, 'rb') as avFile:
  1384. mediaBinary = avFile.read()
  1385. self._set_headers('image/'+mediaFileType,len(mediaBinary),cookie)
  1386. self._write(mediaBinary)
  1387. return
  1388. self._404()
  1389. return
  1390. self._benchmarkGETtimings(GETstartTime,GETtimings,21)
  1391. # icon images
  1392. # Note that this comes before the busy flag to avoid conflicts
  1393. if self.path.startswith('/icons/'):
  1394. if self.path.endswith('.png'):
  1395. mediaStr=self.path.split('/icons/')[1]
  1396. mediaFilename= \
  1397. self.server.baseDir+'/img/icons/'+mediaStr
  1398. if self.server.iconsCache.get(mediaStr):
  1399. mediaBinary=self.server.iconsCache[mediaStr]
  1400. self._set_headers('image/png',len(mediaBinary),cookie)
  1401. self._write(mediaBinary)
  1402. return
  1403. else:
  1404. if os.path.isfile(mediaFilename):
  1405. with open(mediaFilename, 'rb') as avFile:
  1406. mediaBinary = avFile.read()
  1407. self._set_headers('image/png',len(mediaBinary),cookie)
  1408. self._write(mediaBinary)
  1409. self.server.iconsCache[mediaStr]=mediaBinary
  1410. return
  1411. self._404()
  1412. return
  1413. self._benchmarkGETtimings(GETstartTime,GETtimings,22)
  1414. # cached avatar images
  1415. # Note that this comes before the busy flag to avoid conflicts
  1416. if self.path.startswith('/avatars/'):
  1417. mediaFilename= \
  1418. self.server.baseDir+'/cache/'+self.path
  1419. if os.path.isfile(mediaFilename):
  1420. with open(mediaFilename, 'rb') as avFile:
  1421. mediaBinary = avFile.read()
  1422. if mediaFilename.endswith('.png'):
  1423. self._set_headers('image/png',len(mediaBinary),cookie)
  1424. elif mediaFilename.endswith('.jpg'):
  1425. self._set_headers('image/jpeg',len(mediaBinary),cookie)
  1426. elif mediaFilename.endswith('.gif'):
  1427. self._set_headers('image/gif',len(mediaBinary),cookie)
  1428. else:
  1429. # default to jpeg
  1430. self._set_headers('image/jpeg',len(mediaBinary),cookie)
  1431. #self._404()
  1432. return
  1433. self._write(mediaBinary)
  1434. return
  1435. self._404()
  1436. return
  1437. self._benchmarkGETtimings(GETstartTime,GETtimings,23)
  1438. # show avatar or background image
  1439. # Note that this comes before the busy flag to avoid conflicts
  1440. if '/users/' in self.path:
  1441. if self.path.endswith('.png') or \
  1442. self.path.endswith('.jpg') or \
  1443. self.path.endswith('.webp') or \
  1444. self.path.endswith('.gif'):
  1445. avatarStr=self.path.split('/users/')[1]
  1446. if '/' in avatarStr and '.temp.' not in self.path:
  1447. avatarNickname=avatarStr.split('/')[0]
  1448. avatarFile=avatarStr.split('/')[1]
  1449. avatarFilename= \
  1450. self.server.baseDir+'/accounts/'+ \
  1451. avatarNickname+'@'+ \
  1452. self.server.domain+'/'+avatarFile
  1453. if os.path.isfile(avatarFilename):
  1454. mediaImageType='png'
  1455. if avatarFile.endswith('.png'):
  1456. mediaImageType='png'
  1457. elif avatarFile.endswith('.jpg'):
  1458. mediaImageType='jpeg'
  1459. elif avatarFile.endswith('.gif'):
  1460. mediaImageType='gif'
  1461. else:
  1462. mediaImageType='webp'
  1463. with open(avatarFilename, 'rb') as avFile:
  1464. mediaBinary = avFile.read()
  1465. self._set_headers('image/'+mediaImageType, \
  1466. len(mediaBinary),cookie)
  1467. self._write(mediaBinary)
  1468. return
  1469. self._benchmarkGETtimings(GETstartTime,GETtimings,24)
  1470. # This busy state helps to avoid flooding
  1471. # Resources which are expected to be called from a web page
  1472. # should be above this
  1473. if self.server.GETbusy:
  1474. currTimeGET=int(time.time())
  1475. if currTimeGET-self.server.lastGET==0:
  1476. if self.server.debug:
  1477. print('DEBUG: GET Busy')
  1478. self.send_response(429)
  1479. self.end_headers()
  1480. return
  1481. self.server.lastGET=currTimeGET
  1482. self.server.GETbusy=True
  1483. self._benchmarkGETtimings(GETstartTime,GETtimings,25)
  1484. if not self._permittedDir(self.path):
  1485. if self.server.debug:
  1486. print('DEBUG: GET Not permitted')
  1487. self._404()
  1488. self.server.GETbusy=False
  1489. return
  1490. # get webfinger endpoint for a person
  1491. if self._webfinger():
  1492. self.server.GETbusy=False
  1493. return
  1494. self._benchmarkGETtimings(GETstartTime,GETtimings,26)
  1495. if self.path.startswith('/login') or \
  1496. (self.path=='/' and not authorized):
  1497. # request basic auth
  1498. msg=htmlLogin(self.server.translate, \
  1499. self.server.baseDir).encode('utf-8')
  1500. self._login_headers('text/html',len(msg))
  1501. self._write(msg)
  1502. self.server.GETbusy=False
  1503. return
  1504. self._benchmarkGETtimings(GETstartTime,GETtimings,27)
  1505. # hashtag search
  1506. if self.path.startswith('/tags/'):
  1507. pageNumber=1
  1508. if '?page=' in self.path:
  1509. pageNumberStr=self.path.split('?page=')[1]
  1510. if pageNumberStr.isdigit():
  1511. pageNumber=int(pageNumberStr)
  1512. hashtag=self.path.split('/tags/')[1]
  1513. if '?page=' in hashtag:
  1514. hashtag=hashtag.split('?page=')[0]
  1515. if isBlockedHashtag(self.server.baseDir,hashtag):
  1516. msg=htmlHashtagBlocked(self.server.baseDir).encode('utf-8')
  1517. self._login_headers('text/html',len(msg))
  1518. self._write(msg)
  1519. self.server.GETbusy=False
  1520. return
  1521. hashtagStr= \
  1522. htmlHashtagSearch(self.server.domain,self.server.port, \
  1523. self.server.recentPostsCache, \
  1524. self.server.maxRecentPosts, \
  1525. self.server.translate, \
  1526. self.server.baseDir,hashtag,pageNumber, \
  1527. maxPostsInFeed,self.server.session, \
  1528. self.server.cachedWebfingers, \
  1529. self.server.personCache, \
  1530. self.server.httpPrefix, \
  1531. self.server.projectVersion)
  1532. if hashtagStr:
  1533. msg=hashtagStr.encode()
  1534. self._set_headers('text/html',len(msg),cookie)
  1535. self._write(msg)
  1536. else:
  1537. originPathStr=self.path.split('/tags/')[0]
  1538. originPathStrAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+originPathStr
  1539. self._redirect_headers(originPathStrAbsolute+'/search',cookie)
  1540. self.server.GETbusy=False
  1541. return
  1542. self._benchmarkGETtimings(GETstartTime,GETtimings,28)
  1543. # search for a fediverse address, shared item or emoji
  1544. # from the web interface by selecting search icon
  1545. if htmlGET and '/users/' in self.path:
  1546. if self.path.endswith('/search'):
  1547. # show the search screen
  1548. msg=htmlSearch(self.server.translate, \
  1549. self.server.baseDir,self.path).encode()
  1550. self._set_headers('text/html',len(msg),cookie)
  1551. self._write(msg)
  1552. self.server.GETbusy=False
  1553. return
  1554. self._benchmarkGETtimings(GETstartTime,GETtimings,29)
  1555. # Show the calendar for a user
  1556. if htmlGET and '/users/' in self.path:
  1557. if '/calendar' in self.path:
  1558. # show the calendar screen
  1559. msg=htmlCalendar(self.server.translate, \
  1560. self.server.baseDir,self.path, \
  1561. self.server.httpPrefix, \
  1562. self.server.domainFull).encode()
  1563. self._set_headers('text/html',len(msg),cookie)
  1564. self._write(msg)
  1565. self.server.GETbusy=False
  1566. return
  1567. self._benchmarkGETtimings(GETstartTime,GETtimings,30)
  1568. # search for emoji by name
  1569. if htmlGET and '/users/' in self.path:
  1570. if self.path.endswith('/searchemoji'):
  1571. # show the search screen
  1572. msg=htmlSearchEmojiTextEntry(self.server.translate, \
  1573. self.server.baseDir, \
  1574. self.path).encode()
  1575. self._set_headers('text/html',len(msg),cookie)
  1576. self._write(msg)
  1577. self.server.GETbusy=False
  1578. return
  1579. self._benchmarkGETtimings(GETstartTime,GETtimings,31)
  1580. # announce/repeat from the web interface
  1581. if htmlGET and '?repeat=' in self.path:
  1582. pageNumber=1
  1583. repeatUrl=self.path.split('?repeat=')[1]
  1584. if '?' in repeatUrl:
  1585. repeatUrl=repeatUrl.split('?')[0]
  1586. timelineBookmark=''
  1587. if '?bm=' in self.path:
  1588. timelineBookmark=self.path.split('?bm=')[1]
  1589. if '?' in timelineBookmark:
  1590. timelineBookmark=timelineBookmark.split('?')[0]
  1591. timelineBookmark='#'+timelineBookmark
  1592. if '?page=' in self.path:
  1593. pageNumberStr=self.path.split('?page=')[1]
  1594. if '?' in pageNumberStr:
  1595. pageNumberStr=pageNumberStr.split('?')[0]
  1596. if pageNumberStr.isdigit():
  1597. pageNumber=int(pageNumberStr)
  1598. timelineStr='inbox'
  1599. if '?tl=' in self.path:
  1600. timelineStr=self.path.split('?tl=')[1]
  1601. if '?' in timelineStr:
  1602. timelineStr=timelineStr.split('?')[0]
  1603. actor=self.path.split('?repeat=')[0]
  1604. self.postToNickname=getNicknameFromActor(actor)
  1605. if not self.postToNickname:
  1606. print('WARN: unable to find nickname in '+actor)
  1607. self.server.GETbusy=False
  1608. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1609. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1610. '?page='+str(pageNumber),cookie)
  1611. return
  1612. if not self.server.session:
  1613. self.server.session= \
  1614. createSession(self.server.useTor)
  1615. self.server.actorRepeat=self.path.split('?actor=')[1]
  1616. announceJson= \
  1617. createAnnounce(self.server.session, \
  1618. self.server.baseDir, \
  1619. self.server.federationList, \
  1620. self.postToNickname, \
  1621. self.server.domain,self.server.port, \
  1622. 'https://www.w3.org/ns/activitystreams#Public', \
  1623. None,self.server.httpPrefix, \
  1624. repeatUrl,False,False, \
  1625. self.server.sendThreads, \
  1626. self.server.postLog, \
  1627. self.server.personCache, \
  1628. self.server.cachedWebfingers, \
  1629. self.server.debug, \
  1630. self.server.projectVersion)
  1631. if announceJson:
  1632. self._postToOutboxThread(announceJson)
  1633. self.server.GETbusy=False
  1634. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1635. self._redirect_headers(actorAbsolute+'/'+timelineStr+'?page='+ \
  1636. str(pageNumber)+ \
  1637. timelineBookmark,cookie)
  1638. return
  1639. self._benchmarkGETtimings(GETstartTime,GETtimings,32)
  1640. # undo an announce/repeat from the web interface
  1641. if htmlGET and '?unrepeat=' in self.path:
  1642. pageNumber=1
  1643. repeatUrl=self.path.split('?unrepeat=')[1]
  1644. if '?' in repeatUrl:
  1645. repeatUrl=repeatUrl.split('?')[0]
  1646. timelineBookmark=''
  1647. if '?bm=' in self.path:
  1648. timelineBookmark=self.path.split('?bm=')[1]
  1649. if '?' in timelineBookmark:
  1650. timelineBookmark=timelineBookmark.split('?')[0]
  1651. timelineBookmark='#'+timelineBookmark
  1652. if '?page=' in self.path:
  1653. pageNumberStr=self.path.split('?page=')[1]
  1654. if '?' in pageNumberStr:
  1655. pageNumberStr=pageNumberStr.split('?')[0]
  1656. if pageNumberStr.isdigit():
  1657. pageNumber=int(pageNumberStr)
  1658. timelineStr='inbox'
  1659. if '?tl=' in self.path:
  1660. timelineStr=self.path.split('?tl=')[1]
  1661. if '?' in timelineStr:
  1662. timelineStr=timelineStr.split('?')[0]
  1663. actor=self.path.split('?unrepeat=')[0]
  1664. self.postToNickname=getNicknameFromActor(actor)
  1665. if not self.postToNickname:
  1666. print('WARN: unable to find nickname in '+actor)
  1667. self.server.GETbusy=False
  1668. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1669. self._redirect_headers(actorAbsolute+'/'+timelineStr+'?page='+ \
  1670. str(pageNumber),cookie)
  1671. return
  1672. if not self.server.session:
  1673. self.server.session= \
  1674. createSession(self.server.useTor)
  1675. undoAnnounceActor= \
  1676. self.server.httpPrefix+'://'+self.server.domainFull+ \
  1677. '/users/'+self.postToNickname
  1678. newUndoAnnounce = {
  1679. "@context": "https://www.w3.org/ns/activitystreams",
  1680. 'actor': undoAnnounceActor,
  1681. 'type': 'Undo',
  1682. 'cc': [undoAnnounceActor+'/followers'],
  1683. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  1684. 'object': {
  1685. 'actor': undoAnnounceActor,
  1686. 'cc': [undoAnnounceActor+'/followers'],
  1687. 'object': repeatUrl,
  1688. 'to': ['https://www.w3.org/ns/activitystreams#Public'],
  1689. 'type': 'Announce'
  1690. }
  1691. }
  1692. self._postToOutboxThread(newUndoAnnounce)
  1693. self.server.GETbusy=False
  1694. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1695. self._redirect_headers(actorAbsolute+'/'+timelineStr+'?page='+ \
  1696. str(pageNumber)+ \
  1697. timelineBookmark,cookie)
  1698. return
  1699. self._benchmarkGETtimings(GETstartTime,GETtimings,33)
  1700. # send a follow request approval from the web interface
  1701. if authorized and '/followapprove=' in self.path and \
  1702. self.path.startswith('/users/'):
  1703. originPathStr=self.path.split('/followapprove=')[0]
  1704. followerNickname=originPathStr.replace('/users/','')
  1705. followingHandle=self.path.split('/followapprove=')[1]
  1706. if '@' in followingHandle:
  1707. if not self.server.session:
  1708. self.server.session= \
  1709. createSession(self.server.useTor)
  1710. manualApproveFollowRequest(self.server.session, \
  1711. self.server.baseDir, \
  1712. self.server.httpPrefix, \
  1713. followerNickname, \
  1714. self.server.domain, \
  1715. self.server.port, \
  1716. followingHandle, \
  1717. self.server.federationList, \
  1718. self.server.sendThreads, \
  1719. self.server.postLog, \
  1720. self.server.cachedWebfingers, \
  1721. self.server.personCache, \
  1722. self.server.acceptedCaps, \
  1723. self.server.debug, \
  1724. self.server.projectVersion)
  1725. originPathStrAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+originPathStr
  1726. self._redirect_headers(originPathStrAbsolute,cookie)
  1727. self.server.GETbusy=False
  1728. return
  1729. self._benchmarkGETtimings(GETstartTime,GETtimings,34)
  1730. # deny a follow request from the web interface
  1731. if authorized and '/followdeny=' in self.path and \
  1732. self.path.startswith('/users/'):
  1733. originPathStr=self.path.split('/followdeny=')[0]
  1734. followerNickname=originPathStr.replace('/users/','')
  1735. followingHandle=self.path.split('/followdeny=')[1]
  1736. if '@' in followingHandle:
  1737. manualDenyFollowRequest(self.server.session, \
  1738. self.server.baseDir, \
  1739. self.server.httpPrefix, \
  1740. followerNickname, \
  1741. self.server.domain, \
  1742. self.server.port, \
  1743. followingHandle, \
  1744. self.server.federationList, \
  1745. self.server.sendThreads, \
  1746. self.server.postLog, \
  1747. self.server.cachedWebfingers, \
  1748. self.server.personCache, \
  1749. self.server.debug, \
  1750. self.server.projectVersion)
  1751. originPathStrAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+originPathStr
  1752. self._redirect_headers(originPathStrAbsolute,cookie)
  1753. self.server.GETbusy=False
  1754. return
  1755. self._benchmarkGETtimings(GETstartTime,GETtimings,35)
  1756. # like from the web interface icon
  1757. if htmlGET and '?like=' in self.path:
  1758. pageNumber=1
  1759. likeUrl=self.path.split('?like=')[1]
  1760. if '?' in likeUrl:
  1761. likeUrl=likeUrl.split('?')[0]
  1762. timelineBookmark=''
  1763. if '?bm=' in self.path:
  1764. timelineBookmark=self.path.split('?bm=')[1]
  1765. if '?' in timelineBookmark:
  1766. timelineBookmark=timelineBookmark.split('?')[0]
  1767. timelineBookmark='#'+timelineBookmark
  1768. actor=self.path.split('?like=')[0]
  1769. if '?page=' in self.path:
  1770. pageNumberStr=self.path.split('?page=')[1]
  1771. if '?' in pageNumberStr:
  1772. pageNumberStr=pageNumberStr.split('?')[0]
  1773. if pageNumberStr.isdigit():
  1774. pageNumber=int(pageNumberStr)
  1775. timelineStr='inbox'
  1776. if '?tl=' in self.path:
  1777. timelineStr=self.path.split('?tl=')[1]
  1778. if '?' in timelineStr:
  1779. timelineStr=timelineStr.split('?')[0]
  1780. self.postToNickname=getNicknameFromActor(actor)
  1781. if not self.postToNickname:
  1782. print('WARN: unable to find nickname in '+actor)
  1783. self.server.GETbusy=False
  1784. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1785. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1786. '?page='+str(pageNumber)+ \
  1787. timelineBookmark,cookie)
  1788. return
  1789. if not self.server.session:
  1790. self.server.session= \
  1791. createSession(self.server.useTor)
  1792. likeActor= \
  1793. self.server.httpPrefix+'://'+ \
  1794. self.server.domainFull+'/users/'+self.postToNickname
  1795. actorLiked=self.path.split('?actor=')[1]
  1796. if '?' in actorLiked:
  1797. actorLiked=actorLiked.split('?')[0]
  1798. likeJson= {
  1799. "@context": "https://www.w3.org/ns/activitystreams",
  1800. 'type': 'Like',
  1801. 'actor': likeActor,
  1802. 'to': [actorLiked],
  1803. 'object': likeUrl
  1804. }
  1805. self._postToOutbox(likeJson,self.server.projectVersion)
  1806. self.server.GETbusy=False
  1807. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1808. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1809. '?page='+str(pageNumber)+ \
  1810. timelineBookmark,cookie)
  1811. return
  1812. self._benchmarkGETtimings(GETstartTime,GETtimings,36)
  1813. # undo a like from the web interface icon
  1814. if htmlGET and '?unlike=' in self.path:
  1815. pageNumber=1
  1816. likeUrl=self.path.split('?unlike=')[1]
  1817. if '?' in likeUrl:
  1818. likeUrl=likeUrl.split('?')[0]
  1819. timelineBookmark=''
  1820. if '?bm=' in self.path:
  1821. timelineBookmark=self.path.split('?bm=')[1]
  1822. if '?' in timelineBookmark:
  1823. timelineBookmark=timelineBookmark.split('?')[0]
  1824. timelineBookmark='#'+timelineBookmark
  1825. if '?page=' in self.path:
  1826. pageNumberStr=self.path.split('?page=')[1]
  1827. if '?' in pageNumberStr:
  1828. pageNumberStr=pageNumberStr.split('?')[0]
  1829. if pageNumberStr.isdigit():
  1830. pageNumber=int(pageNumberStr)
  1831. timelineStr='inbox'
  1832. if '?tl=' in self.path:
  1833. timelineStr=self.path.split('?tl=')[1]
  1834. if '?' in timelineStr:
  1835. timelineStr=timelineStr.split('?')[0]
  1836. actor=self.path.split('?unlike=')[0]
  1837. self.postToNickname=getNicknameFromActor(actor)
  1838. if not self.postToNickname:
  1839. print('WARN: unable to find nickname in '+actor)
  1840. self.server.GETbusy=False
  1841. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1842. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1843. '?page='+str(pageNumber),cookie)
  1844. return
  1845. if not self.server.session:
  1846. self.server.session= \
  1847. createSession(self.server.useTor)
  1848. undoActor= \
  1849. self.server.httpPrefix+'://'+ \
  1850. self.server.domainFull+'/users/'+self.postToNickname
  1851. actorLiked=self.path.split('?actor=')[1]
  1852. if '?' in actorLiked:
  1853. actorLiked=actorLiked.split('?')[0]
  1854. undoLikeJson= {
  1855. "@context": "https://www.w3.org/ns/activitystreams",
  1856. 'type': 'Undo',
  1857. 'actor': undoActor,
  1858. 'to': [actorLiked],
  1859. 'object': {
  1860. 'type': 'Like',
  1861. 'actor': undoActor,
  1862. 'to': [actorLiked],
  1863. 'object': likeUrl
  1864. }
  1865. }
  1866. self._postToOutbox(undoLikeJson,self.server.projectVersion)
  1867. self.server.GETbusy=False
  1868. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1869. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1870. '?page='+str(pageNumber)+ \
  1871. timelineBookmark,cookie)
  1872. return
  1873. self._benchmarkGETtimings(GETstartTime,GETtimings,36)
  1874. # bookmark from the web interface icon
  1875. if htmlGET and '?bookmark=' in self.path:
  1876. pageNumber=1
  1877. bookmarkUrl=self.path.split('?bookmark=')[1]
  1878. if '?' in bookmarkUrl:
  1879. bookmarkUrl=bookmarkUrl.split('?')[0]
  1880. timelineBookmark=''
  1881. if '?bm=' in self.path:
  1882. timelineBookmark=self.path.split('?bm=')[1]
  1883. if '?' in timelineBookmark:
  1884. timelineBookmark=timelineBookmark.split('?')[0]
  1885. timelineBookmark='#'+timelineBookmark
  1886. actor=self.path.split('?bookmark=')[0]
  1887. if '?page=' in self.path:
  1888. pageNumberStr=self.path.split('?page=')[1]
  1889. if '?' in pageNumberStr:
  1890. pageNumberStr=pageNumberStr.split('?')[0]
  1891. if pageNumberStr.isdigit():
  1892. pageNumber=int(pageNumberStr)
  1893. timelineStr='inbox'
  1894. if '?tl=' in self.path:
  1895. timelineStr=self.path.split('?tl=')[1]
  1896. if '?' in timelineStr:
  1897. timelineStr=timelineStr.split('?')[0]
  1898. self.postToNickname=getNicknameFromActor(actor)
  1899. if not self.postToNickname:
  1900. print('WARN: unable to find nickname in '+actor)
  1901. self.server.GETbusy=False
  1902. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1903. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1904. '?page='+str(pageNumber),cookie)
  1905. return
  1906. if not self.server.session:
  1907. self.server.session= \
  1908. createSession(self.server.useTor)
  1909. bookmarkActor= \
  1910. self.server.httpPrefix+'://'+ \
  1911. self.server.domainFull+'/users/'+self.postToNickname
  1912. bookmarkJson= {
  1913. "@context": "https://www.w3.org/ns/activitystreams",
  1914. 'type': 'Bookmark',
  1915. 'actor': bookmarkActor,
  1916. 'to': [bookmarkActor],
  1917. 'object': bookmarkUrl
  1918. }
  1919. self._postToOutbox(bookmarkJson,self.server.projectVersion)
  1920. self.server.GETbusy=False
  1921. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1922. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1923. '?page='+str(pageNumber)+ \
  1924. timelineBookmark,cookie)
  1925. return
  1926. # undo a bookmark from the web interface icon
  1927. if htmlGET and '?unbookmark=' in self.path:
  1928. pageNumber=1
  1929. bookmarkUrl=self.path.split('?unbookmark=')[1]
  1930. if '?' in bookmarkUrl:
  1931. bookmarkUrl=bookmarkUrl.split('?')[0]
  1932. timelineBookmark=''
  1933. if '?bm=' in self.path:
  1934. timelineBookmark=self.path.split('?bm=')[1]
  1935. if '?' in timelineBookmark:
  1936. timelineBookmark=timelineBookmark.split('?')[0]
  1937. timelineBookmark='#'+timelineBookmark
  1938. if '?page=' in self.path:
  1939. pageNumberStr=self.path.split('?page=')[1]
  1940. if '?' in pageNumberStr:
  1941. pageNumberStr=pageNumberStr.split('?')[0]
  1942. if pageNumberStr.isdigit():
  1943. pageNumber=int(pageNumberStr)
  1944. timelineStr='inbox'
  1945. if '?tl=' in self.path:
  1946. timelineStr=self.path.split('?tl=')[1]
  1947. if '?' in timelineStr:
  1948. timelineStr=timelineStr.split('?')[0]
  1949. actor=self.path.split('?unbookmark=')[0]
  1950. self.postToNickname=getNicknameFromActor(actor)
  1951. if not self.postToNickname:
  1952. print('WARN: unable to find nickname in '+actor)
  1953. self.server.GETbusy=False
  1954. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1955. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1956. '?page='+str(pageNumber),cookie)
  1957. return
  1958. if not self.server.session:
  1959. self.server.session= \
  1960. createSession(self.server.useTor)
  1961. undoActor= \
  1962. self.server.httpPrefix+'://'+ \
  1963. self.server.domainFull+'/users/'+self.postToNickname
  1964. undoBookmarkJson= {
  1965. "@context": "https://www.w3.org/ns/activitystreams",
  1966. 'type': 'Undo',
  1967. 'actor': undoActor,
  1968. 'to': [undoActor],
  1969. 'object': {
  1970. 'type': 'Bookmark',
  1971. 'actor': undoActor,
  1972. 'to': [undoActor],
  1973. 'object': bookmarkUrl
  1974. }
  1975. }
  1976. self._postToOutbox(undoBookmarkJson,self.server.projectVersion)
  1977. self.server.GETbusy=False
  1978. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  1979. self._redirect_headers(actorAbsolute+'/'+timelineStr+ \
  1980. '?page='+str(pageNumber)+ \
  1981. timelineBookmark,cookie)
  1982. return
  1983. self._benchmarkGETtimings(GETstartTime,GETtimings,37)
  1984. # delete a post from the web interface icon
  1985. if htmlGET and '?delete=' in self.path:
  1986. pageNumber=1
  1987. if '?page=' in self.path:
  1988. pageNumberStr=self.path.split('?page=')[1]
  1989. if '?' in pageNumberStr:
  1990. pageNumberStr=pageNumberStr.split('?')[0]
  1991. if pageNumberStr.isdigit():
  1992. pageNumber=int(pageNumberStr)
  1993. deleteUrl=self.path.split('?delete=')[1]
  1994. if '?' in deleteUrl:
  1995. deleteUrl=deleteUrl.split('?')[0]
  1996. timelineStr=self.server.defaultTimeline
  1997. if '?tl=' in self.path:
  1998. timelineStr=self.path.split('?tl=')[1]
  1999. if '?' in timelineStr:
  2000. timelineStr=timelineStr.split('?')[0]
  2001. actor= \
  2002. self.server.httpPrefix+'://'+ \
  2003. self.server.domainFull+self.path.split('?delete=')[0]
  2004. if self.server.allowDeletion or \
  2005. deleteUrl.startswith(actor):
  2006. if self.server.debug:
  2007. print('DEBUG: deleteUrl='+deleteUrl)
  2008. print('DEBUG: actor='+actor)
  2009. if actor not in deleteUrl:
  2010. # You can only delete your own posts
  2011. self.server.GETbusy=False
  2012. self._redirect_headers(actor+'/'+timelineStr,cookie)
  2013. return
  2014. self.postToNickname=getNicknameFromActor(actor)
  2015. if not self.postToNickname:
  2016. print('WARN: unable to find nickname in '+actor)
  2017. self.server.GETbusy=False
  2018. self._redirect_headers(actor+'/'+timelineStr,cookie)
  2019. return
  2020. if not self.server.session:
  2021. self.server.session= \
  2022. createSession(self.server.useTor)
  2023. deleteStr= \
  2024. htmlDeletePost(self.server.recentPostsCache, \
  2025. self.server.maxRecentPosts, \
  2026. self.server.translate,pageNumber, \
  2027. self.server.session,self.server.baseDir, \
  2028. deleteUrl,self.server.httpPrefix, \
  2029. __version__,self.server.cachedWebfingers, \
  2030. self.server.personCache)
  2031. if deleteStr:
  2032. self._set_headers('text/html',len(deleteStr),cookie)
  2033. self._write(deleteStr.encode())
  2034. self.server.GETbusy=False
  2035. return
  2036. self.server.GETbusy=False
  2037. self._redirect_headers(actor+'/'+timelineStr,cookie)
  2038. return
  2039. # mute a post from the web interface icon
  2040. if htmlGET and '?mute=' in self.path:
  2041. pageNumber=1
  2042. if '?page=' in self.path:
  2043. pageNumberStr=self.path.split('?page=')[1]
  2044. if '?' in pageNumberStr:
  2045. pageNumberStr=pageNumberStr.split('?')[0]
  2046. if pageNumberStr.isdigit():
  2047. pageNumber=int(pageNumberStr)
  2048. muteUrl=self.path.split('?mute=')[1]
  2049. if '?' in muteUrl:
  2050. muteUrl=muteUrl.split('?')[0]
  2051. timelineBookmark=''
  2052. if '?bm=' in self.path:
  2053. timelineBookmark=self.path.split('?bm=')[1]
  2054. if '?' in timelineBookmark:
  2055. timelineBookmark=timelineBookmark.split('?')[0]
  2056. timelineBookmark='#'+timelineBookmark
  2057. timelineStr=self.server.defaultTimeline
  2058. if '?tl=' in self.path:
  2059. timelineStr=self.path.split('?tl=')[1]
  2060. if '?' in timelineStr:
  2061. timelineStr=timelineStr.split('?')[0]
  2062. actor= \
  2063. self.server.httpPrefix+'://'+ \
  2064. self.server.domainFull+self.path.split('?mute=')[0]
  2065. nickname=getNicknameFromActor(actor)
  2066. mutePost(self.server.baseDir,nickname,self.server.domain, \
  2067. muteUrl,self.server.recentPostsCache)
  2068. self.server.GETbusy=False
  2069. self._redirect_headers(actor+'/'+timelineStr+timelineBookmark,cookie)
  2070. return
  2071. # unmute a post from the web interface icon
  2072. if htmlGET and '?unmute=' in self.path:
  2073. pageNumber=1
  2074. if '?page=' in self.path:
  2075. pageNumberStr=self.path.split('?page=')[1]
  2076. if '?' in pageNumberStr:
  2077. pageNumberStr=pageNumberStr.split('?')[0]
  2078. if pageNumberStr.isdigit():
  2079. pageNumber=int(pageNumberStr)
  2080. muteUrl=self.path.split('?unmute=')[1]
  2081. if '?' in muteUrl:
  2082. muteUrl=muteUrl.split('?')[0]
  2083. timelineBookmark=''
  2084. if '?bm=' in self.path:
  2085. timelineBookmark=self.path.split('?bm=')[1]
  2086. if '?' in timelineBookmark:
  2087. timelineBookmark=timelineBookmark.split('?')[0]
  2088. timelineBookmark='#'+timelineBookmark
  2089. timelineStr=self.server.defaultTimeline
  2090. if '?tl=' in self.path:
  2091. timelineStr=self.path.split('?tl=')[1]
  2092. if '?' in timelineStr:
  2093. timelineStr=timelineStr.split('?')[0]
  2094. actor= \
  2095. self.server.httpPrefix+'://'+ \
  2096. self.server.domainFull+self.path.split('?unmute=')[0]
  2097. nickname=getNicknameFromActor(actor)
  2098. unmutePost(self.server.baseDir,nickname,self.server.domain, \
  2099. muteUrl,self.server.recentPostsCache)
  2100. self.server.GETbusy=False
  2101. self._redirect_headers(actor+'/'+timelineStr+timelineBookmark,cookie)
  2102. return
  2103. # reply from the web interface icon
  2104. inReplyToUrl=None
  2105. replyWithDM=False
  2106. replyToList=[]
  2107. replyPageNumber=1
  2108. shareDescription=None
  2109. replytoActor=None
  2110. if htmlGET:
  2111. # public reply
  2112. if '?replyto=' in self.path:
  2113. inReplyToUrl=self.path.split('?replyto=')[1]
  2114. if '?' in inReplyToUrl:
  2115. mentionsList=inReplyToUrl.split('?')
  2116. for m in mentionsList:
  2117. if m.startswith('mention='):
  2118. replyHandle=m.replace('mention=','')
  2119. if replyHandle not in replyToList:
  2120. replyToList.append(replyHandle)
  2121. if m.startswith('page='):
  2122. replyPageStr=m.replace('page=','')
  2123. if replyPageStr.isdigit():
  2124. replyPageNumber=int(replyPageStr)
  2125. if m.startswith('actor='):
  2126. replytoActor=m.replace('actor=','')
  2127. inReplyToUrl=mentionsList[0]
  2128. self.path=self.path.split('?replyto=')[0]+'/newpost'
  2129. if self.server.debug:
  2130. print('DEBUG: replyto path '+self.path)
  2131. # reply to followers
  2132. if '?replyfollowers=' in self.path:
  2133. inReplyToUrl=self.path.split('?replyfollowers=')[1]
  2134. if '?' in inReplyToUrl:
  2135. mentionsList=inReplyToUrl.split('?')
  2136. for m in mentionsList:
  2137. if m.startswith('mention='):
  2138. replyHandle=m.replace('mention=','')
  2139. if m.replace('mention=','') not in replyToList:
  2140. replyToList.append(replyHandle)
  2141. if m.startswith('page='):
  2142. replyPageStr=m.replace('page=','')
  2143. if replyPageStr.isdigit():
  2144. replyPageNumber=int(replyPageStr)
  2145. if m.startswith('actor='):
  2146. replytoActor=m.replace('actor=','')
  2147. inReplyToUrl=mentionsList[0]
  2148. self.path=self.path.split('?replyfollowers=')[0]+'/newfollowers'
  2149. if self.server.debug:
  2150. print('DEBUG: replyfollowers path '+self.path)
  2151. # replying as a direct message, for moderation posts or the dm timeline
  2152. if '?replydm=' in self.path:
  2153. inReplyToUrl=self.path.split('?replydm=')[1]
  2154. if '?' in inReplyToUrl:
  2155. mentionsList=inReplyToUrl.split('?')
  2156. for m in mentionsList:
  2157. if m.startswith('mention='):
  2158. replyHandle=m.replace('mention=','')
  2159. if m.replace('mention=','') not in replyToList:
  2160. replyToList.append(m.replace('mention=',''))
  2161. if m.startswith('page='):
  2162. replyPageStr=m.replace('page=','')
  2163. if replyPageStr.isdigit():
  2164. replyPageNumber=int(replyPageStr)
  2165. if m.startswith('actor='):
  2166. replytoActor=m.replace('actor=','')
  2167. inReplyToUrl=mentionsList[0]
  2168. if inReplyToUrl.startswith('sharedesc:'):
  2169. shareDescription= \
  2170. inReplyToUrl.replace('sharedesc:','').replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#')
  2171. self.path=self.path.split('?replydm=')[0]+'/newdm'
  2172. if self.server.debug:
  2173. print('DEBUG: replydm path '+self.path)
  2174. # edit profile in web interface
  2175. if '/users/' in self.path and self.path.endswith('/editprofile'):
  2176. msg=htmlEditProfile(self.server.translate, \
  2177. self.server.baseDir, \
  2178. self.path,self.server.domain, \
  2179. self.server.port, \
  2180. self.server.httpPrefix).encode()
  2181. self._set_headers('text/html',len(msg),cookie)
  2182. self._write(msg)
  2183. self.server.GETbusy=False
  2184. return
  2185. # Various types of new post in the web interface
  2186. if '/users/' in self.path and \
  2187. (self.path.endswith('/newpost') or \
  2188. self.path.endswith('/newunlisted') or \
  2189. self.path.endswith('/newfollowers') or \
  2190. self.path.endswith('/newdm') or \
  2191. self.path.endswith('/newreport') or \
  2192. self.path.endswith('/newquestion') or \
  2193. self.path.endswith('/newshare')):
  2194. nickname=getNicknameFromActor(self.path)
  2195. msg=htmlNewPost(self.server.mediaInstance, \
  2196. self.server.translate, \
  2197. self.server.baseDir, \
  2198. self.server.httpPrefix, \
  2199. self.path,inReplyToUrl, \
  2200. replyToList, \
  2201. shareDescription, \
  2202. replyPageNumber, \
  2203. nickname,self.server.domain).encode()
  2204. self._set_headers('text/html',len(msg),cookie)
  2205. self._write(msg)
  2206. self.server.GETbusy=False
  2207. return
  2208. self._benchmarkGETtimings(GETstartTime,GETtimings,38)
  2209. # get an individual post from the path /@nickname/statusnumber
  2210. if '/@' in self.path:
  2211. namedStatus=self.path.split('/@')[1]
  2212. if '/' not in namedStatus:
  2213. # show actor
  2214. nickname=namedStatus
  2215. else:
  2216. postSections=namedStatus.split('/')
  2217. if len(postSections)==2:
  2218. nickname=postSections[0]
  2219. statusNumber=postSections[1]
  2220. if len(statusNumber)>10 and statusNumber.isdigit():
  2221. postFilename= \
  2222. self.server.baseDir+'/accounts/'+nickname+'@'+ \
  2223. self.server.domain+'/outbox/'+ \
  2224. self.server.httpPrefix+':##'+ \
  2225. self.server.domainFull+'#users#'+ \
  2226. nickname+'#statuses#'+statusNumber+'.json'
  2227. if os.path.isfile(postFilename):
  2228. postJsonObject=loadJson(postFilename)
  2229. loadedPost=False
  2230. if postJsonObject:
  2231. loadedPost=True
  2232. else:
  2233. postJsonObject={}
  2234. if loadedPost:
  2235. # Only authorized viewers get to see likes on posts
  2236. # Otherwize marketers could gain more social graph info
  2237. if not authorized:
  2238. self._removePostInteractions(postJsonObject)
  2239. if self._requestHTTP():
  2240. msg= \
  2241. htmlIndividualPost(self.server.recentPostsCache, \
  2242. self.server.maxRecentPosts, \
  2243. self.server.translate, \
  2244. self.server.session, \
  2245. self.server.cachedWebfingers, \
  2246. self.server.personCache, \
  2247. nickname,self.server.domain, \
  2248. self.server.port, \
  2249. authorized,postJsonObject, \
  2250. self.server.httpPrefix, \
  2251. self.server.projectVersion).encode('utf-8')
  2252. self._set_headers('text/html',len(msg),cookie)
  2253. self._write(msg)
  2254. else:
  2255. if self._fetchAuthenticated():
  2256. msg=json.dumps(postJsonObject,ensure_ascii=False).encode('utf-8')
  2257. self._set_headers('application/json',len(msg),None)
  2258. self._write(msg)
  2259. else:
  2260. self._404()
  2261. self.server.GETbusy=False
  2262. return
  2263. else:
  2264. self._404()
  2265. self.server.GETbusy=False
  2266. return
  2267. self._benchmarkGETtimings(GETstartTime,GETtimings,39)
  2268. # get replies to a post /users/nickname/statuses/number/replies
  2269. if self.path.endswith('/replies') or '/replies?page=' in self.path:
  2270. if '/statuses/' in self.path and '/users/' in self.path:
  2271. namedStatus=self.path.split('/users/')[1]
  2272. if '/' in namedStatus:
  2273. postSections=namedStatus.split('/')
  2274. if len(postSections)>=4:
  2275. if postSections[3].startswith('replies'):
  2276. nickname=postSections[0]
  2277. statusNumber=postSections[2]
  2278. if len(statusNumber)>10 and statusNumber.isdigit():
  2279. #get the replies file
  2280. boxname='outbox'
  2281. postDir= \
  2282. self.server.baseDir+'/accounts/'+ \
  2283. nickname+'@'+self.server.domain+'/'+boxname
  2284. postRepliesFilename= \
  2285. postDir+'/'+ \
  2286. self.server.httpPrefix+':##'+ \
  2287. self.server.domainFull+'#users#'+ \
  2288. nickname+'#statuses#'+statusNumber+'.replies'
  2289. if not os.path.isfile(postRepliesFilename):
  2290. # There are no replies, so show empty collection
  2291. repliesJson = {
  2292. '@context': 'https://www.w3.org/ns/activitystreams',
  2293. 'first': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
  2294. 'id': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies',
  2295. 'last': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'/replies?page=true',
  2296. 'totalItems': 0,
  2297. 'type': 'OrderedCollection'}
  2298. if self._requestHTTP():
  2299. if not self.server.session:
  2300. if self.server.debug:
  2301. print('DEBUG: creating new session')
  2302. self.server.session= \
  2303. createSession(self.server.useTor)
  2304. msg=htmlPostReplies(self.server.recentPostsCache, \
  2305. self.server.maxRecentPosts, \
  2306. self.server.translate, \
  2307. self.server.baseDir, \
  2308. self.server.session, \
  2309. self.server.cachedWebfingers, \
  2310. self.server.personCache, \
  2311. nickname, \
  2312. self.server.domain, \
  2313. self.server.port, \
  2314. repliesJson, \
  2315. self.server.httpPrefix, \
  2316. self.server.projectVersion).encode('utf-8')
  2317. self._set_headers('text/html',len(msg),cookie)
  2318. print('----------------------------------------------------')
  2319. self._write(msg)
  2320. else:
  2321. if self._fetchAuthenticated():
  2322. msg=json.dumps(repliesJson,ensure_ascii=False).encode('utf-8')
  2323. self._set_headers('application/json',len(msg),None)
  2324. self._write(msg)
  2325. else:
  2326. self._404()
  2327. self.server.GETbusy=False
  2328. return
  2329. else:
  2330. # replies exist. Itterate through the text file containing message ids
  2331. repliesJson = {
  2332. '@context': 'https://www.w3.org/ns/activitystreams',
  2333. 'id': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber+'?page=true',
  2334. 'orderedItems': [
  2335. ],
  2336. 'partOf': self.server.httpPrefix+'://'+self.server.domainFull+'/users/'+nickname+'/statuses/'+statusNumber,
  2337. 'type': 'OrderedCollectionPage'}
  2338. # populate the items list with replies
  2339. populateRepliesJson(self.server.baseDir, \
  2340. nickname, \
  2341. self.server.domain, \
  2342. postRepliesFilename, \
  2343. authorized, \
  2344. repliesJson)
  2345. # send the replies json
  2346. if self._requestHTTP():
  2347. if not self.server.session:
  2348. if self.server.debug:
  2349. print('DEBUG: creating new session')
  2350. self.server.session= \
  2351. createSession(self.server.useTor)
  2352. msg=htmlPostReplies(self.server.recentPostsCache, \
  2353. self.server.maxRecentPosts, \
  2354. self.server.translate, \
  2355. self.server.baseDir, \
  2356. self.server.session, \
  2357. self.server.cachedWebfingers, \
  2358. self.server.personCache, \
  2359. nickname, \
  2360. self.server.domain, \
  2361. self.server.port, \
  2362. repliesJson, \
  2363. self.server.httpPrefix, \
  2364. self.server.projectVersion).encode('utf-8')
  2365. self._set_headers('text/html',len(msg),cookie)
  2366. self._write(msg)
  2367. else:
  2368. if self._fetchAuthenticated():
  2369. msg=json.dumps(repliesJson,ensure_ascii=False).encode('utf-8')
  2370. self._set_headers('application/json',len(msg),None)
  2371. self._write(msg)
  2372. else:
  2373. self._404()
  2374. self.server.GETbusy=False
  2375. return
  2376. self._benchmarkGETtimings(GETstartTime,GETtimings,40)
  2377. if self.path.endswith('/roles') and '/users/' in self.path:
  2378. namedStatus=self.path.split('/users/')[1]
  2379. if '/' in namedStatus:
  2380. postSections=namedStatus.split('/')
  2381. nickname=postSections[0]
  2382. actorFilename= \
  2383. self.server.baseDir+'/accounts/'+ \
  2384. nickname+'@'+self.server.domain+'.json'
  2385. if os.path.isfile(actorFilename):
  2386. actorJson=loadJson(actorFilename)
  2387. if actorJson:
  2388. if actorJson.get('roles'):
  2389. if self._requestHTTP():
  2390. getPerson = \
  2391. personLookup(self.server.domain, \
  2392. self.path.replace('/roles',''), \
  2393. self.server.baseDir)
  2394. if getPerson:
  2395. msg=htmlProfile(self.server.defaultTimeline, \
  2396. self.server.recentPostsCache, \
  2397. self.server.maxRecentPosts, \
  2398. self.server.translate, \
  2399. self.server.projectVersion, \
  2400. self.server.baseDir, \
  2401. self.server.httpPrefix, \
  2402. True, \
  2403. self.server.ocapAlways, \
  2404. getPerson,'roles', \
  2405. self.server.session, \
  2406. self.server.cachedWebfingers, \
  2407. self.server.personCache, \
  2408. actorJson['roles'], \
  2409. None,None).encode('utf-8')
  2410. self._set_headers('text/html',len(msg),cookie)
  2411. self._write(msg)
  2412. else:
  2413. if self._fetchAuthenticated():
  2414. msg=json.dumps(actorJson['roles'],ensure_ascii=False).encode('utf-8')
  2415. self._set_headers('application/json',len(msg),None)
  2416. self._write(msg)
  2417. else:
  2418. self._404()
  2419. self.server.GETbusy=False
  2420. return
  2421. # show skills on the profile page
  2422. if self.path.endswith('/skills') and '/users/' in self.path:
  2423. namedStatus=self.path.split('/users/')[1]
  2424. if '/' in namedStatus:
  2425. postSections=namedStatus.split('/')
  2426. nickname=postSections[0]
  2427. actorFilename= \
  2428. self.server.baseDir+'/accounts/'+ \
  2429. nickname+'@'+self.server.domain+'.json'
  2430. if os.path.isfile(actorFilename):
  2431. actorJson=loadJson(actorFilename)
  2432. if actorJson:
  2433. if actorJson.get('skills'):
  2434. if self._requestHTTP():
  2435. getPerson = \
  2436. personLookup(self.server.domain, \
  2437. self.path.replace('/skills',''), \
  2438. self.server.baseDir)
  2439. if getPerson:
  2440. msg=htmlProfile(self.server.defaultTimeline, \
  2441. self.server.recentPostsCache, \
  2442. self.server.maxRecentPosts, \
  2443. self.server.translate, \
  2444. self.server.projectVersion, \
  2445. self.server.baseDir, \
  2446. self.server.httpPrefix, \
  2447. True, \
  2448. self.server.ocapAlways, \
  2449. getPerson,'skills', \
  2450. self.server.session, \
  2451. self.server.cachedWebfingers, \
  2452. self.server.personCache, \
  2453. actorJson['skills'], \
  2454. None,None).encode('utf-8')
  2455. self._set_headers('text/html',len(msg),cookie)
  2456. self._write(msg)
  2457. else:
  2458. if self._fetchAuthenticated():
  2459. msg=json.dumps(actorJson['skills'],ensure_ascii=False).encode('utf-8')
  2460. self._set_headers('application/json',len(msg),None)
  2461. self._write(msg)
  2462. else:
  2463. self._404()
  2464. self.server.GETbusy=False
  2465. return
  2466. actor=self.path.replace('/skills','')
  2467. actorAbsolute=self.server.httpPrefix+'://'+self.server.domainFull+actor
  2468. self._redirect_headers(actorAbsolute,cookie)
  2469. self.server.GETbusy=False
  2470. return
  2471. self._benchmarkGETtimings(GETstartTime,GETtimings,41)
  2472. # get an individual post from the path /users/nickname/statuses/number
  2473. if '/statuses/' in self.path and '/users/' in self.path:
  2474. namedStatus=self.path.split('/users/')[1]
  2475. if '/' in namedStatus:
  2476. postSections=namedStatus.split('/')
  2477. if len(postSections)>=3:
  2478. nickname=postSections[0]
  2479. statusNumber=postSections[2]
  2480. if len(statusNumber)>10 and statusNumber.isdigit():
  2481. postFilename= \
  2482. self.server.baseDir+'/accounts/'+ \
  2483. nickname+'@'+self.server.domain+'/outbox/'+ \
  2484. self.server.httpPrefix+':##'+ \
  2485. self.server.domainFull+'#users#'+ \
  2486. nickname+'#statuses#'+statusNumber+'.json'
  2487. if os.path.isfile(postFilename):
  2488. postJsonObject=loadJson(postFilename)
  2489. if not postJsonObject:
  2490. self.send_response(429)
  2491. self.end_headers()
  2492. self.server.GETbusy=False
  2493. return
  2494. else:
  2495. # Only authorized viewers get to see likes on posts
  2496. # Otherwize marketers could gain more social graph info
  2497. if not authorized:
  2498. self._removePostInteractions(postJsonObject)
  2499. if self._requestHTTP():
  2500. msg=htmlIndividualPost(self.server.recentPostsCache, \
  2501. self.server.maxRecentPosts, \
  2502. self.server.translate, \
  2503. self.server.baseDir, \
  2504. self.server.session, \
  2505. self.server.cachedWebfingers, \
  2506. self.server.personCache, \
  2507. nickname, \
  2508. self.server.domain, \
  2509. self.server.port, \
  2510. authorized,postJsonObject, \
  2511. self.server.httpPrefix, \
  2512. self.server.projectVersion).encode('utf-8')
  2513. self._set_headers('text/html',len(msg),cookie)
  2514. self._write(msg)
  2515. else:
  2516. if self._fetchAuthenticated():
  2517. msg=json.dumps(postJsonObject,ensure_ascii=False).encode('utf-8')
  2518. self._set_headers('application/json',len(msg),None)
  2519. self._write(msg)
  2520. else:
  2521. self._404()
  2522. self.server.GETbusy=False
  2523. return
  2524. else:
  2525. self._404()
  2526. self.server.GETbusy=False
  2527. return
  2528. self._benchmarkGETtimings(GETstartTime,GETtimings,42)
  2529. # get the inbox for a given person
  2530. if self.path.endswith('/inbox') or '/inbox?page=' in self.path:
  2531. if '/users/' in self.path:
  2532. if authorized:
  2533. inboxFeed= \
  2534. personBoxJson(self.server.recentPostsCache, \
  2535. self.server.session, \
  2536. self.server.baseDir, \
  2537. self.server.domain, \
  2538. self.server.port, \
  2539. self.path, \
  2540. self.server.httpPrefix, \
  2541. maxPostsInFeed, 'inbox', \
  2542. authorized,self.server.ocapAlways)
  2543. if inboxFeed:
  2544. if self._requestHTTP():
  2545. nickname=self.path.replace('/users/','').replace('/inbox','')
  2546. pageNumber=1
  2547. if '?page=' in nickname:
  2548. pageNumber=nickname.split('?page=')[1]
  2549. nickname=nickname.split('?page=')[0]
  2550. if pageNumber.isdigit():
  2551. pageNumber=int(pageNumber)
  2552. else:
  2553. pageNumber=1
  2554. if 'page=' not in self.path:
  2555. # if no page was specified then show the first
  2556. inboxFeed= \
  2557. personBoxJson(self.server.recentPostsCache, \
  2558. self.server.session, \
  2559. self.server.baseDir, \
  2560. self.server.domain, \
  2561. self.server.port, \
  2562. self.path+'?page=1', \
  2563. self.server.httpPrefix, \
  2564. maxPostsInFeed, 'inbox', \
  2565. authorized,self.server.ocapAlways)
  2566. msg=htmlInbox(self.server.defaultTimeline, \
  2567. self.server.recentPostsCache, \
  2568. self.server.maxRecentPosts, \
  2569. self.server.translate, \
  2570. pageNumber,maxPostsInFeed, \
  2571. self.server.session, \
  2572. self.server.baseDir, \
  2573. self.server.cachedWebfingers, \
  2574. self.server.personCache, \
  2575. nickname, \
  2576. self.server.domain, \
  2577. self.server.port, \
  2578. inboxFeed, \
  2579. self.server.allowDeletion, \
  2580. self.server.httpPrefix, \
  2581. self.server.projectVersion).encode('utf-8')
  2582. self._set_headers('text/html',len(msg),cookie)
  2583. self._write(msg)
  2584. else:
  2585. # don't need authenticated fetch here because there is
  2586. # already the authorization check
  2587. msg=json.dumps(inboxFeed,ensure_ascii=False).encode('utf-8')
  2588. self._set_headers('application/json',len(msg),None)
  2589. self._write(msg)
  2590. self.server.GETbusy=False
  2591. return
  2592. else:
  2593. if self.server.debug:
  2594. nickname=self.path.replace('/users/','').replace('/inbox','')
  2595. print('DEBUG: '+nickname+ \
  2596. ' was not authorized to access '+self.path)
  2597. if self.path!='/inbox':
  2598. # not the shared inbox
  2599. if self.server.debug:
  2600. print('DEBUG: GET access to inbox is unauthorized')
  2601. self.send_response(405)
  2602. self.end_headers()
  2603. self.server.GETbusy=False
  2604. return
  2605. self._benchmarkGETtimings(GETstartTime,GETtimings,43)
  2606. # get the direct messages for a given person
  2607. if self.path.endswith('/dm') or '/dm?page=' in self.path:
  2608. if '/users/' in self.path:
  2609. if authorized:
  2610. inboxDMFeed= \
  2611. personBoxJson(self.server.recentPostsCache, \
  2612. self.server.session, \
  2613. self.server.baseDir, \
  2614. self.server.domain, \
  2615. self.server.port, \
  2616. self.path, \
  2617. self.server.httpPrefix, \
  2618. maxPostsInFeed, 'dm', \
  2619. authorized,self.server.ocapAlways)
  2620. if inboxDMFeed:
  2621. if self._requestHTTP():
  2622. nickname=self.path.replace('/users/','').replace('/dm','')
  2623. pageNumber=1
  2624. if '?page=' in nickname:
  2625. pageNumber=nickname.split('?page=')[1]
  2626. nickname=nickname.split('?page=')[0]
  2627. if pageNumber.isdigit():
  2628. pageNumber=int(pageNumber)
  2629. else:
  2630. pageNumber=1
  2631. if 'page=' not in self.path:
  2632. # if no page was specified then show the first
  2633. inboxDMFeed= \
  2634. personBoxJson(self.server.recentPostsCache, \
  2635. self.server.session, \
  2636. self.server.baseDir, \
  2637. self.server.domain, \
  2638. self.server.port, \
  2639. self.path+'?page=1', \
  2640. self.server.httpPrefix, \
  2641. maxPostsInFeed, 'dm', \
  2642. authorized,self.server.ocapAlways)
  2643. msg=htmlInboxDMs(self.server.defaultTimeline, \
  2644. self.server.recentPostsCache, \
  2645. self.server.maxRecentPosts, \
  2646. self.server.translate, \
  2647. pageNumber,maxPostsInFeed, \
  2648. self.server.session, \
  2649. self.server.baseDir, \
  2650. self.server.cachedWebfingers, \
  2651. self.server.personCache, \
  2652. nickname, \
  2653. self.server.domain, \
  2654. self.server.port, \
  2655. inboxDMFeed, \
  2656. self.server.allowDeletion, \
  2657. self.server.httpPrefix, \
  2658. self.server.projectVersion).encode('utf-8')
  2659. self._set_headers('text/html',len(msg),cookie)
  2660. self._write(msg)
  2661. else:
  2662. # don't need authenticated fetch here because there is
  2663. # already the authorization check
  2664. msg=json.dumps(inboxDMFeed,ensure_ascii=False).encode('utf-8')
  2665. self._set_headers('application/json',len(msg),None)
  2666. self._write(msg)
  2667. self.server.GETbusy=False
  2668. return
  2669. else:
  2670. if self.server.debug:
  2671. nickname=self.path.replace('/users/','').replace('/dm','')
  2672. print('DEBUG: '+nickname+ \
  2673. ' was not authorized to access '+self.path)
  2674. if self.path!='/dm':
  2675. # not the DM inbox
  2676. if self.server.debug:
  2677. print('DEBUG: GET access to inbox is unauthorized')
  2678. self.send_response(405)
  2679. self.end_headers()
  2680. self.server.GETbusy=False
  2681. return
  2682. self._benchmarkGETtimings(GETstartTime,GETtimings,44)
  2683. # get the replies for a given person
  2684. if self.path.endswith('/tlreplies') or '/tlreplies?page=' in self.path:
  2685. if '/users/' in self.path:
  2686. if authorized:
  2687. inboxRepliesFeed= \
  2688. personBoxJson(self.server.recentPostsCache, \
  2689. self.server.session, \
  2690. self.server.baseDir, \
  2691. self.server.domain, \
  2692. self.server.port, \
  2693. self.path, \
  2694. self.server.httpPrefix, \
  2695. maxPostsInFeed, 'tlreplies', \
  2696. True,self.server.ocapAlways)
  2697. if not inboxRepliesFeed:
  2698. inboxRepliesFeed=[]
  2699. if self._requestHTTP():
  2700. nickname=self.path.replace('/users/','').replace('/tlreplies','')
  2701. pageNumber=1
  2702. if '?page=' in nickname:
  2703. pageNumber=nickname.split('?page=')[1]
  2704. nickname=nickname.split('?page=')[0]
  2705. if pageNumber.isdigit():
  2706. pageNumber=int(pageNumber)
  2707. else:
  2708. pageNumber=1
  2709. if 'page=' not in self.path:
  2710. # if no page was specified then show the first
  2711. inboxRepliesFeed= \
  2712. personBoxJson(self.server.recentPostsCache, \
  2713. self.server.session, \
  2714. self.server.baseDir, \
  2715. self.server.domain, \
  2716. self.server.port, \
  2717. self.path+'?page=1', \
  2718. self.server.httpPrefix, \
  2719. maxPostsInFeed, 'tlreplies', \
  2720. True,self.server.ocapAlways)
  2721. msg=htmlInboxReplies(self.server.defaultTimeline, \
  2722. self.server.recentPostsCache, \
  2723. self.server.maxRecentPosts, \
  2724. self.server.translate, \
  2725. pageNumber,maxPostsInFeed, \
  2726. self.server.session, \
  2727. self.server.baseDir, \
  2728. self.server.cachedWebfingers, \
  2729. self.server.personCache, \
  2730. nickname, \
  2731. self.server.domain, \
  2732. self.server.port, \
  2733. inboxRepliesFeed, \
  2734. self.server.allowDeletion, \
  2735. self.server.httpPrefix, \
  2736. self.server.projectVersion).encode('utf-8')
  2737. self._set_headers('text/html',len(msg),cookie)
  2738. self._write(msg)
  2739. else:
  2740. # don't need authenticated fetch here because there is
  2741. # already the authorization check
  2742. msg=json.dumps(inboxRepliesFeed,ensure_ascii=False).encode('utf-8')
  2743. self._set_headers('application/json',len(msg),None)
  2744. self._write(msg)
  2745. self.server.GETbusy=False
  2746. return
  2747. else:
  2748. if self.server.debug:
  2749. nickname=self.path.replace('/users/','').replace('/tlreplies','')
  2750. print('DEBUG: '+nickname+ \
  2751. ' was not authorized to access '+self.path)
  2752. if self.path!='/tlreplies':
  2753. # not the replies inbox
  2754. if self.server.debug:
  2755. print('DEBUG: GET access to inbox is unauthorized')
  2756. self.send_response(405)
  2757. self.end_headers()
  2758. self.server.GETbusy=False
  2759. return
  2760. self._benchmarkGETtimings(GETstartTime,GETtimings,45)
  2761. # get the media for a given person
  2762. if self.path.endswith('/tlmedia') or '/tlmedia?page=' in self.path:
  2763. if '/users/' in self.path:
  2764. if authorized:
  2765. inboxMediaFeed= \
  2766. personBoxJson(self.server.recentPostsCache, \
  2767. self.server.session, \
  2768. self.server.baseDir, \
  2769. self.server.domain, \
  2770. self.server.port, \
  2771. self.path, \
  2772. self.server.httpPrefix, \
  2773. maxPostsInMediaFeed, 'tlmedia', \
  2774. True,self.server.ocapAlways)
  2775. if not inboxMediaFeed:
  2776. inboxMediaFeed=[]
  2777. if self._requestHTTP():
  2778. nickname=self.path.replace('/users/','').replace('/tlmedia','')
  2779. pageNumber=1
  2780. if '?page=' in nickname:
  2781. pageNumber=nickname.split('?page=')[1]
  2782. nickname=nickname.split('?page=')[0]
  2783. if pageNumber.isdigit():
  2784. pageNumber=int(pageNumber)
  2785. else:
  2786. pageNumber=1
  2787. if 'page=' not in self.path:
  2788. # if no page was specified then show the first
  2789. inboxMediaFeed= \
  2790. personBoxJson(self.server.recentPostsCache, \
  2791. self.server.session, \
  2792. self.server.baseDir, \
  2793. self.server.domain, \
  2794. self.server.port, \
  2795. self.path+'?page=1', \
  2796. self.server.httpPrefix, \
  2797. maxPostsInMediaFeed, 'tlmedia', \
  2798. True,self.server.ocapAlways)
  2799. msg=htmlInboxMedia(self.server.defaultTimeline, \
  2800. self.server.recentPostsCache, \
  2801. self.server.maxRecentPosts, \
  2802. self.server.translate, \
  2803. pageNumber,maxPostsInMediaFeed, \
  2804. self.server.session, \
  2805. self.server.baseDir, \
  2806. self.server.cachedWebfingers, \
  2807. self.server.personCache, \
  2808. nickname, \
  2809. self.server.domain, \
  2810. self.server.port, \
  2811. inboxMediaFeed, \
  2812. self.server.allowDeletion, \
  2813. self.server.httpPrefix, \
  2814. self.server.projectVersion).encode('utf-8')
  2815. self._set_headers('text/html',len(msg),cookie)
  2816. self._write(msg)
  2817. else:
  2818. # don't need authenticated fetch here because there is
  2819. # already the authorization check
  2820. msg=json.dumps(inboxMediaFeed,ensure_ascii=False).encode('utf-8')
  2821. self._set_headers('application/json',len(msg),None)
  2822. self._write(msg)
  2823. self.server.GETbusy=False
  2824. return
  2825. else:
  2826. if self.server.debug:
  2827. nickname=self.path.replace('/users/','').replace('/tlmedia','')
  2828. print('DEBUG: '+nickname+ \
  2829. ' was not authorized to access '+self.path)
  2830. if self.path!='/tlmedia':
  2831. # not the media inbox
  2832. if self.server.debug:
  2833. print('DEBUG: GET access to inbox is unauthorized')
  2834. self.send_response(405)
  2835. self.end_headers()
  2836. self.server.GETbusy=False
  2837. return
  2838. self._benchmarkGETtimings(GETstartTime,GETtimings,46)
  2839. # get the shared items timeline for a given person
  2840. if self.path.endswith('/tlshares') or '/tlshares?page=' in self.path:
  2841. if '/users/' in self.path:
  2842. if authorized:
  2843. if self._requestHTTP():
  2844. nickname=self.path.replace('/users/','').replace('/tlshares','')
  2845. pageNumber=1
  2846. if '?page=' in nickname:
  2847. pageNumber=nickname.split('?page=')[1]
  2848. nickname=nickname.split('?page=')[0]
  2849. if pageNumber.isdigit():
  2850. pageNumber=int(pageNumber)
  2851. else:
  2852. pageNumber=1
  2853. msg=htmlShares(self.server.defaultTimeline, \
  2854. self.server.recentPostsCache, \
  2855. self.server.maxRecentPosts, \
  2856. self.server.translate, \
  2857. pageNumber,maxPostsInFeed, \
  2858. self.server.session, \
  2859. self.server.baseDir, \
  2860. self.server.cachedWebfingers, \
  2861. self.server.personCache, \
  2862. nickname, \
  2863. self.server.domain, \
  2864. self.server.port, \
  2865. self.server.allowDeletion, \
  2866. self.server.httpPrefix, \
  2867. self.server.projectVersion).encode('utf-8')
  2868. self._set_headers('text/html',len(msg),cookie)
  2869. self._write(msg)
  2870. self.server.GETbusy=False
  2871. return
  2872. # not the shares timeline
  2873. if self.server.debug:
  2874. print('DEBUG: GET access to shares timeline is unauthorized')
  2875. self.send_response(405)
  2876. self.end_headers()
  2877. self.server.GETbusy=False
  2878. return
  2879. # get the bookmarks for a given person
  2880. if self.path.endswith('/tlbookmarks') or '/tlbookmarks?page=' in self.path:
  2881. if '/users/' in self.path:
  2882. if authorized:
  2883. bookmarksFeed= \
  2884. personBoxJson(self.server.recentPostsCache, \
  2885. self.server.session, \
  2886. self.server.baseDir, \
  2887. self.server.domain, \
  2888. self.server.port, \
  2889. self.path, \
  2890. self.server.httpPrefix, \
  2891. maxPostsInFeed, 'tlbookmarks', \
  2892. authorized,self.server.ocapAlways)
  2893. if bookmarksFeed:
  2894. if self._requestHTTP():
  2895. nickname=self.path.replace('/users/','').replace('/tlbookmarks','')
  2896. pageNumber=1
  2897. if '?page=' in nickname:
  2898. pageNumber=nickname.split('?page=')[1]
  2899. nickname=nickname.split('?page=')[0]
  2900. if pageNumber.isdigit():
  2901. pageNumber=int(pageNumber)
  2902. else:
  2903. pageNumber=1
  2904. if 'page=' not in self.path:
  2905. # if no page was specified then show the first
  2906. bookmarksFeed= \
  2907. personBoxJson(self.server.recentPostsCache, \
  2908. self.server.session, \
  2909. self.server.baseDir, \
  2910. self.server.domain, \
  2911. self.server.port, \
  2912. self.path+'?page=1', \
  2913. self.server.httpPrefix, \
  2914. maxPostsInFeed, 'tlbookmarks', \
  2915. authorized,self.server.ocapAlways)
  2916. msg=htmlBookmarks(self.server.defaultTimeline, \
  2917. self.server.recentPostsCache, \
  2918. self.server.maxRecentPosts, \
  2919. self.server.translate, \
  2920. pageNumber,maxPostsInFeed, \
  2921. self.server.session, \
  2922. self.server.baseDir, \
  2923. self.server.cachedWebfingers, \
  2924. self.server.personCache, \
  2925. nickname, \
  2926. self.server.domain, \
  2927. self.server.port, \
  2928. bookmarksFeed, \
  2929. self.server.allowDeletion, \
  2930. self.server.httpPrefix, \
  2931. self.server.projectVersion).encode('utf-8')
  2932. self._set_headers('text/html',len(msg),cookie)
  2933. self._write(msg)
  2934. else:
  2935. # don't need authenticated fetch here because there is
  2936. # already the authorization check
  2937. msg=json.dumps(inboxFeed,ensure_ascii=False).encode('utf-8')
  2938. self._set_headers('application/json',len(msg),None)
  2939. self._write(msg)
  2940. self.server.GETbusy=False
  2941. return
  2942. else:
  2943. if self.server.debug:
  2944. nickname=self.path.replace('/users/','').replace('/tlbookmarks','')
  2945. print('DEBUG: '+nickname+ \
  2946. ' was not authorized to access '+self.path)
  2947. if self.server.debug:
  2948. print('DEBUG: GET access to bookmarks is unauthorized')
  2949. self.send_response(405)
  2950. self.end_headers()
  2951. self.server.GETbusy=False
  2952. return
  2953. self._benchmarkGETtimings(GETstartTime,GETtimings,47)
  2954. # get outbox feed for a person
  2955. outboxFeed= \
  2956. personBoxJson(self.server.recentPostsCache, \
  2957. self.server.session, \
  2958. self.server.baseDir,self.server.domain, \
  2959. self.server.port,self.path, \
  2960. self.server.httpPrefix, \
  2961. maxPostsInFeed, 'outbox', \
  2962. authorized, \
  2963. self.server.ocapAlways)
  2964. if outboxFeed:
  2965. if self._requestHTTP():
  2966. nickname= \
  2967. self.path.replace('/users/','').replace('/outbox','')
  2968. pageNumber=1
  2969. if '?page=' in nickname:
  2970. pageNumber=nickname.split('?page=')[1]
  2971. nickname=nickname.split('?page=')[0]
  2972. if pageNumber.isdigit():
  2973. pageNumber=int(pageNumber)
  2974. else:
  2975. pageNumber=1
  2976. if 'page=' not in self.path:
  2977. # if a page wasn't specified then show the first one
  2978. outboxFeed= \
  2979. personBoxJson(self.server.recentPostsCache, \
  2980. self.server.session, \
  2981. self.server.baseDir, \
  2982. self.server.domain, \
  2983. self.server.port, \
  2984. self.path+'?page=1', \
  2985. self.server.httpPrefix, \
  2986. maxPostsInFeed, 'outbox', \
  2987. authorized, \
  2988. self.server.ocapAlways)
  2989. msg=htmlOutbox(self.server.defaultTimeline, \
  2990. self.server.recentPostsCache, \
  2991. self.server.maxRecentPosts, \
  2992. self.server.translate, \
  2993. pageNumber,maxPostsInFeed, \
  2994. self.server.session, \
  2995. self.server.baseDir, \
  2996. self.server.cachedWebfingers, \
  2997. self.server.personCache, \
  2998. nickname, \
  2999. self.server.domain, \
  3000. self.server.port, \
  3001. outboxFeed, \
  3002. self.server.allowDeletion, \
  3003. self.server.httpPrefix, \
  3004. self.server.projectVersion).encode('utf-8')
  3005. self._set_headers('text/html',len(msg),cookie)
  3006. self._write(msg)
  3007. else:
  3008. if self._fetchAuthenticated():
  3009. msg=json.dumps(outboxFeed,ensure_ascii=False).encode('utf-8')
  3010. self._set_headers('application/json',len(msg),None)
  3011. self._write(msg)
  3012. else:
  3013. self._404()
  3014. self.server.GETbusy=False
  3015. return
  3016. self._benchmarkGETtimings(GETstartTime,GETtimings,48)
  3017. # get the moderation feed for a moderator
  3018. if self.path.endswith('/moderation') or \
  3019. '/moderation?page=' in self.path:
  3020. if '/users/' in self.path:
  3021. if authorized:
  3022. moderationFeed= \
  3023. personBoxJson(self.server.recentPostsCache, \
  3024. self.server.session, \
  3025. self.server.baseDir, \
  3026. self.server.domain, \
  3027. self.server.port, \
  3028. self.path, \
  3029. self.server.httpPrefix, \
  3030. maxPostsInFeed, 'moderation', \
  3031. True,self.server.ocapAlways)
  3032. if moderationFeed:
  3033. if self._requestHTTP():
  3034. nickname= \
  3035. self.path.replace('/users/','').replace('/moderation','')
  3036. pageNumber=1
  3037. if '?page=' in nickname:
  3038. pageNumber=nickname.split('?page=')[1]
  3039. nickname=nickname.split('?page=')[0]
  3040. if pageNumber.isdigit():
  3041. pageNumber=int(pageNumber)
  3042. else:
  3043. pageNumber=1
  3044. if 'page=' not in self.path:
  3045. # if no page was specified then show the first
  3046. moderationFeed= \
  3047. personBoxJson(self.server.recentPostsCache, \
  3048. self.server.session, \
  3049. self.server.baseDir, \
  3050. self.server.domain, \
  3051. self.server.port, \
  3052. self.path+'?page=1', \
  3053. self.server.httpPrefix, \
  3054. maxPostsInFeed, 'moderation', \
  3055. True,self.server.ocapAlways)
  3056. msg=htmlModeration(self.server.defaultTimeline, \
  3057. self.server.recentPostsCache, \
  3058. self.server.maxRecentPosts, \
  3059. self.server.translate, \
  3060. pageNumber,maxPostsInFeed, \
  3061. self.server.session, \
  3062. self.server.baseDir, \
  3063. self.server.cachedWebfingers, \
  3064. self.server.personCache, \
  3065. nickname, \
  3066. self.server.domain, \
  3067. self.server.port, \
  3068. moderationFeed, \
  3069. True, \
  3070. self.server.httpPrefix, \
  3071. self.server.projectVersion).encode('utf-8')
  3072. self._set_headers('text/html',len(msg),cookie)
  3073. self._write(msg)
  3074. else:
  3075. # don't need authenticated fetch here because there is
  3076. # already the authorization check
  3077. msg=json.dumps(moderationFeed,ensure_ascii=False).encode('utf-8')
  3078. self._set_headers('application/json',len(msg),None)
  3079. self._write(msg)
  3080. self.server.GETbusy=False
  3081. return
  3082. else:
  3083. if self.server.debug:
  3084. nickname=self.path.replace('/users/','').replace('/moderation','')
  3085. print('DEBUG: '+nickname+ \
  3086. ' was not authorized to access '+self.path)
  3087. if self.server.debug:
  3088. print('DEBUG: GET access to moderation feed is unauthorized')
  3089. self.send_response(405)
  3090. self.end_headers()
  3091. self.server.GETbusy=False
  3092. return
  3093. self._benchmarkGETtimings(GETstartTime,GETtimings,49)
  3094. shares= \
  3095. getSharesFeedForPerson(self.server.baseDir, \
  3096. self.server.domain, \
  3097. self.server.port,self.path, \
  3098. self.server.httpPrefix, \
  3099. sharesPerPage)
  3100. if shares:
  3101. if self._requestHTTP():
  3102. pageNumber=1
  3103. if '?page=' not in self.path:
  3104. searchPath=self.path
  3105. # get a page of shares, not the summary
  3106. shares= \
  3107. getSharesFeedForPerson(self.server.baseDir, \
  3108. self.server.domain, \
  3109. self.server.port, \
  3110. self.path+'?page=true', \
  3111. self.server.httpPrefix, \
  3112. sharesPerPage)
  3113. else:
  3114. pageNumberStr=self.path.split('?page=')[1]
  3115. if pageNumberStr.isdigit():
  3116. pageNumber=int(pageNumberStr)
  3117. searchPath=self.path.split('?page=')[0]
  3118. getPerson= \
  3119. personLookup(self.server.domain, \
  3120. searchPath.replace('/shares',''), \
  3121. self.server.baseDir)
  3122. if getPerson:
  3123. if not self.server.session:
  3124. if self.server.debug:
  3125. print('DEBUG: creating new session')
  3126. self.server.session= \
  3127. createSession(self.server.useTor)
  3128. msg=htmlProfile(self.server.defaultTimeline, \
  3129. self.server.recentPostsCache, \
  3130. self.server.maxRecentPosts, \
  3131. self.server.translate, \
  3132. self.server.projectVersion, \
  3133. self.server.baseDir, \
  3134. self.server.httpPrefix, \
  3135. authorized, \
  3136. self.server.ocapAlways, \
  3137. getPerson,'shares', \
  3138. self.server.session, \
  3139. self.server.cachedWebfingers, \
  3140. self.server.personCache, \
  3141. shares, \
  3142. pageNumber,sharesPerPage).encode('utf-8')
  3143. self._set_headers('text/html',len(msg),cookie)
  3144. self._write(msg)
  3145. self.server.GETbusy=False
  3146. return
  3147. else:
  3148. if self._fetchAuthenticated():
  3149. msg=json.dumps(shares,ensure_ascii=False).encode('utf-8')
  3150. self._set_headers('application/json',len(msg),None)
  3151. self._write(msg)
  3152. else:
  3153. self._404()
  3154. self.server.GETbusy=False
  3155. return
  3156. self._benchmarkGETtimings(GETstartTime,GETtimings,50)
  3157. following=getFollowingFeed(self.server.baseDir,self.server.domain, \
  3158. self.server.port,self.path, \
  3159. self.server.httpPrefix, \
  3160. authorized,followsPerPage)
  3161. if following:
  3162. if self._requestHTTP():
  3163. pageNumber=1
  3164. if '?page=' not in self.path:
  3165. searchPath=self.path
  3166. # get a page of following, not the summary
  3167. following= \
  3168. getFollowingFeed(self.server.baseDir,self.server.domain, \
  3169. self.server.port,self.path+'?page=true', \
  3170. self.server.httpPrefix, \
  3171. authorized,followsPerPage)
  3172. else:
  3173. pageNumberStr=self.path.split('?page=')[1]
  3174. if pageNumberStr.isdigit():
  3175. pageNumber=int(pageNumberStr)
  3176. searchPath=self.path.split('?page=')[0]
  3177. getPerson = personLookup(self.server.domain,searchPath.replace('/following',''), \
  3178. self.server.baseDir)
  3179. if getPerson:
  3180. if not self.server.session:
  3181. if self.server.debug:
  3182. print('DEBUG: creating new session')
  3183. self.server.session= \
  3184. createSession(self.server.useTor)
  3185. msg=htmlProfile(self.server.defaultTimeline, \
  3186. self.server.recentPostsCache, \
  3187. self.server.maxRecentPosts, \
  3188. self.server.translate, \
  3189. self.server.projectVersion, \
  3190. self.server.baseDir, \
  3191. self.server.httpPrefix, \
  3192. authorized, \
  3193. self.server.ocapAlways, \
  3194. getPerson,'following', \
  3195. self.server.session, \
  3196. self.server.cachedWebfingers, \
  3197. self.server.personCache, \
  3198. following, \
  3199. pageNumber,followsPerPage).encode('utf-8')
  3200. self._set_headers('text/html',len(msg),cookie)
  3201. self._write(msg)
  3202. self.server.GETbusy=False
  3203. return
  3204. else:
  3205. if self._fetchAuthenticated():
  3206. msg=json.dumps(following,ensure_ascii=False).encode('utf-8')
  3207. self._set_headers('application/json',len(msg),None)
  3208. self._write(msg)
  3209. else:
  3210. self._404()
  3211. self.server.GETbusy=False
  3212. return
  3213. self._benchmarkGETtimings(GETstartTime,GETtimings,51)
  3214. followers= \
  3215. getFollowingFeed(self.server.baseDir,self.server.domain, \
  3216. self.server.port,self.path, \
  3217. self.server.httpPrefix, \
  3218. authorized,followsPerPage,'followers')
  3219. if followers:
  3220. if self._requestHTTP():
  3221. pageNumber=1
  3222. if '?page=' not in self.path:
  3223. searchPath=self.path
  3224. # get a page of followers, not the summary
  3225. followers= \
  3226. getFollowingFeed(self.server.baseDir,self.server.domain, \
  3227. self.server.port,self.path+'?page=1', \
  3228. self.server.httpPrefix, \
  3229. authorized,followsPerPage,'followers')
  3230. else:
  3231. pageNumberStr=self.path.split('?page=')[1]
  3232. if pageNumberStr.isdigit():
  3233. pageNumber=int(pageNumberStr)
  3234. searchPath=self.path.split('?page=')[0]
  3235. getPerson= \
  3236. personLookup(self.server.domain,searchPath.replace('/followers',''), \
  3237. self.server.baseDir)
  3238. if getPerson:
  3239. if not self.server.session:
  3240. if self.server.debug:
  3241. print('DEBUG: creating new session')
  3242. self.server.session= \
  3243. createSession(self.server.useTor)
  3244. msg=htmlProfile(self.server.defaultTimeline, \
  3245. self.server.recentPostsCache, \
  3246. self.server.maxRecentPosts, \
  3247. self.server.translate, \
  3248. self.server.projectVersion, \
  3249. self.server.baseDir, \
  3250. self.server.httpPrefix, \
  3251. authorized, \
  3252. self.server.ocapAlways, \
  3253. getPerson,'followers', \
  3254. self.server.session, \
  3255. self.server.cachedWebfingers, \
  3256. self.server.personCache, \
  3257. followers, \
  3258. pageNumber,followsPerPage).encode('utf-8')
  3259. self._set_headers('text/html',len(msg),cookie)
  3260. self._write(msg)
  3261. self.server.GETbusy=False
  3262. return
  3263. else:
  3264. if self._fetchAuthenticated():
  3265. msg=json.dumps(followers,ensure_ascii=False).encode('utf-8')
  3266. self._set_headers('application/json',len(msg),None)
  3267. self._write(msg)
  3268. else:
  3269. self._404()
  3270. self.server.GETbusy=False
  3271. return
  3272. self._benchmarkGETtimings(GETstartTime,GETtimings,52)
  3273. # look up a person
  3274. getPerson = personLookup(self.server.domain,self.path, \
  3275. self.server.baseDir)
  3276. if getPerson:
  3277. if self._requestHTTP():
  3278. if not self.server.session:
  3279. if self.server.debug:
  3280. print('DEBUG: creating new session')
  3281. self.server.session= \
  3282. createSession(self.server.useTor)
  3283. msg=htmlProfile(self.server.defaultTimeline, \
  3284. self.server.recentPostsCache, \
  3285. self.server.maxRecentPosts, \
  3286. self.server.translate, \
  3287. self.server.projectVersion, \
  3288. self.server.baseDir, \
  3289. self.server.httpPrefix, \
  3290. authorized, \
  3291. self.server.ocapAlways, \
  3292. getPerson,'posts',
  3293. self.server.session, \
  3294. self.server.cachedWebfingers, \
  3295. self.server.personCache, \
  3296. None,None).encode('utf-8')
  3297. self._set_headers('text/html',len(msg),cookie)
  3298. self._write(msg)
  3299. else:
  3300. if self._fetchAuthenticated():
  3301. msg=json.dumps(getPerson,ensure_ascii=False).encode('utf-8')
  3302. self._set_headers('application/json',len(msg),None)
  3303. self._write(msg)
  3304. else:
  3305. self._404()
  3306. self.server.GETbusy=False
  3307. return
  3308. self._benchmarkGETtimings(GETstartTime,GETtimings,53)
  3309. # check that a json file was requested
  3310. if not self.path.endswith('.json'):
  3311. if self.server.debug:
  3312. print('DEBUG: GET Not json: '+self.path+' '+self.server.baseDir)
  3313. self._404()
  3314. self.server.GETbusy=False
  3315. return
  3316. if not self._fetchAuthenticated():
  3317. if self.server.debug:
  3318. print('WARN: Unauthenticated GET')
  3319. self._404()
  3320. return
  3321. self._benchmarkGETtimings(GETstartTime,GETtimings,54)
  3322. # check that the file exists
  3323. filename=self.server.baseDir+self.path
  3324. if os.path.isfile(filename):
  3325. with open(filename, 'r', encoding='utf-8') as File:
  3326. content = File.read()
  3327. contentJson=json.loads(content)
  3328. msg=json.dumps(contentJson,ensure_ascii=False).encode('utf-8')
  3329. self._set_headers('application/json',len(msg),None)
  3330. self._write(msg)
  3331. else:
  3332. if self.server.debug:
  3333. print('DEBUG: GET Unknown file')
  3334. self._404()
  3335. self.server.GETbusy=False
  3336. self._benchmarkGETtimings(GETstartTime,GETtimings,55)
  3337. def do_HEAD(self):
  3338. checkPath=self.path
  3339. etag=None
  3340. fileLength=-1
  3341. if '/media/' in self.path:
  3342. if self.path.endswith('.png') or \
  3343. self.path.endswith('.jpg') or \
  3344. self.path.endswith('.gif') or \
  3345. self.path.endswith('.webp') or \
  3346. self.path.endswith('.mp4') or \
  3347. self.path.endswith('.ogv') or \
  3348. self.path.endswith('.mp3') or \
  3349. self.path.endswith('.ogg'):
  3350. mediaStr=self.path.split('/media/')[1]
  3351. mediaFilename= \
  3352. self.server.baseDir+'/media/'+mediaStr
  3353. if os.path.isfile(mediaFilename):
  3354. checkPath=mediaFilename
  3355. fileLength=os.path.getsize(mediaFilename)
  3356. if os.path.isfile(mediaFilename+'.etag'):
  3357. try:
  3358. with open(mediaFilename+'.etag', 'r') as etagFile:
  3359. etag = etagFile.read()
  3360. except:
  3361. pass
  3362. else:
  3363. with open(mediaFilename, 'rb') as avFile:
  3364. mediaBinary = avFile.read()
  3365. etag=sha1(mediaBinary).hexdigest()
  3366. try:
  3367. with open(mediaFilename+'.etag', 'w') as etagFile:
  3368. etagFile.write(etag)
  3369. except:
  3370. pass
  3371. mediaFileType='application/json'
  3372. if checkPath.endswith('.png'):
  3373. mediaFileType='image/png'
  3374. elif checkPath.endswith('.jpg'):
  3375. mediaFileType='image/jpeg'
  3376. elif checkPath.endswith('.gif'):
  3377. mediaFileType='image/gif'
  3378. elif checkPath.endswith('.webp'):
  3379. mediaFileType='image/webp'
  3380. elif checkPath.endswith('.mp4'):
  3381. mediaFileType='video/mp4'
  3382. elif checkPath.endswith('.ogv'):
  3383. mediaFileType='video/ogv'
  3384. elif checkPath.endswith('.mp3'):
  3385. mediaFileType='audio/mpeg'
  3386. elif checkPath.endswith('.ogg'):
  3387. mediaFileType='audio/ogg'
  3388. self._set_headers_head(mediaFileType,fileLength,etag)
  3389. def _receiveNewPostProcess(self,authorized: bool, \
  3390. postType: str,path: str,headers: {},
  3391. length: int,postBytes,boundary: str) -> int:
  3392. # Note: this needs to happen synchronously
  3393. # 0 = this is not a new post
  3394. # 1 = new post success
  3395. # -1 = new post failed
  3396. # 2 = new post canceled
  3397. if self.server.debug:
  3398. print('DEBUG: receiving POST')
  3399. if ' boundary=' in headers['Content-Type']:
  3400. if self.server.debug:
  3401. print('DEBUG: receiving POST headers '+headers['Content-Type'])
  3402. nickname=None
  3403. nicknameStr=path.split('/users/')[1]
  3404. if '/' in nicknameStr:
  3405. nickname=nicknameStr.split('/')[0]
  3406. else:
  3407. return -1
  3408. length = int(headers['Content-Length'])
  3409. if length>self.server.maxPostLength:
  3410. print('POST size too large')
  3411. return -1
  3412. boundary=headers['Content-Type'].split('boundary=')[1]
  3413. if ';' in boundary:
  3414. boundary=boundary.split(';')[0]
  3415. # Note: we don't use cgi here because it's due to be deprecated
  3416. # in Python 3.8/3.10
  3417. # Instead we use the multipart mime parser from the email module
  3418. if self.server.debug:
  3419. print('DEBUG: extracting media from POST')
  3420. mediaBytes,postBytes=extractMediaInFormPOST(postBytes,boundary,'attachpic')
  3421. if self.server.debug:
  3422. if mediaBytes:
  3423. print('DEBUG: media was found. '+str(len(mediaBytes))+' bytes')
  3424. else:
  3425. print('DEBUG: no media was found in POST')
  3426. # Note: a .temp extension is used here so that at no time is
  3427. # an image with metadata publicly exposed, even for a few mS
  3428. filenameBase= \
  3429. self.server.baseDir+'/accounts/'+ \
  3430. nickname+'@'+self.server.domain+'/upload.temp'
  3431. filename,attachmentMediaType= \
  3432. saveMediaInFormPOST(mediaBytes,self.server.debug,filenameBase)
  3433. if self.server.debug:
  3434. if filename:
  3435. print('DEBUG: POST media filename is '+filename)
  3436. else:
  3437. print('DEBUG: no media filename in POST')
  3438. if filename:
  3439. if filename.endswith('.png') or \
  3440. filename.endswith('.jpg') or \
  3441. filename.endswith('.webp') or \
  3442. filename.endswith('.gif'):
  3443. if self.server.debug:
  3444. print('DEBUG: POST media removing metadata')
  3445. postImageFilename=filename.replace('.temp','')
  3446. removeMetaData(filename,postImageFilename)
  3447. if os.path.isfile(postImageFilename):
  3448. print('POST media saved to '+postImageFilename)
  3449. else:
  3450. print('ERROR: POST media could not be saved to '+postImageFilename)
  3451. else:
  3452. if os.path.isfile(filename):
  3453. os.rename(filename,filename.replace('.temp',''))
  3454. fields=extractTextFieldsInPOST(postBytes,boundary,self.server.debug)
  3455. if self.server.debug:
  3456. if fields:
  3457. print('DEBUG: text field extracted from POST '+str(fields))
  3458. else:
  3459. print('WARN: no text fields could be extracted from POST')
  3460. # process the received text fields from the POST
  3461. if not fields.get('message') and not fields.get('imageDescription'):
  3462. return -1
  3463. if fields.get('submitPost'):
  3464. if fields['submitPost']!='Submit':
  3465. return -1
  3466. else:
  3467. return 2
  3468. if not fields.get('imageDescription'):
  3469. fields['imageDescription']=None
  3470. if not fields.get('subject'):
  3471. fields['subject']=None
  3472. if not fields.get('replyTo'):
  3473. fields['replyTo']=None
  3474. if not fields.get('eventDate'):
  3475. fields['eventDate']=None
  3476. if not fields.get('eventTime'):
  3477. fields['eventTime']=None
  3478. if not fields.get('location'):
  3479. fields['location']=None
  3480. # Store a file which contains the time in seconds
  3481. # since epoch when an attempt to post something was made.
  3482. # This is then used for active monthly users counts
  3483. lastUsedFilename= \
  3484. self.server.baseDir+'/accounts/'+ \
  3485. nickname+'@'+self.server.domain+'/.lastUsed'
  3486. try:
  3487. lastUsedFile=open(lastUsedFilename,'w')
  3488. if lastUsedFile:
  3489. lastUsedFile.write(str(int(time.time())))
  3490. lastUsedFile.close()
  3491. except:
  3492. pass
  3493. if postType=='newpost':
  3494. messageJson= \
  3495. createPublicPost(self.server.baseDir, \
  3496. nickname, \
  3497. self.server.domain,self.server.port, \
  3498. self.server.httpPrefix, \
  3499. fields['message'],False,False,False, \
  3500. filename,attachmentMediaType, \
  3501. fields['imageDescription'], \
  3502. self.server.useBlurHash, \
  3503. fields['replyTo'],fields['replyTo'], \
  3504. fields['subject'], \
  3505. fields['eventDate'],fields['eventTime'], \
  3506. fields['location'])
  3507. if messageJson:
  3508. self.postToNickname=nickname
  3509. if self._postToOutbox(messageJson,__version__):
  3510. populateReplies(self.server.baseDir, \
  3511. self.server.httpPrefix, \
  3512. self.server.domainFull, \
  3513. messageJson, \
  3514. self.server.maxReplies, \
  3515. self.server.debug)
  3516. return 1
  3517. else:
  3518. return -1
  3519. elif postType=='newunlisted':
  3520. messageJson= \
  3521. createUnlistedPost(self.server.baseDir, \
  3522. nickname, \
  3523. self.server.domain,self.server.port, \
  3524. self.server.httpPrefix, \
  3525. fields['message'],False,False,False, \
  3526. filename,attachmentMediaType, \
  3527. fields['imageDescription'], \
  3528. self.server.useBlurHash, \
  3529. fields['replyTo'], fields['replyTo'], \
  3530. fields['subject'], \
  3531. fields['eventDate'],fields['eventTime'], \
  3532. fields['location'])
  3533. if messageJson:
  3534. self.postToNickname=nickname
  3535. if self._postToOutbox(messageJson,__version__):
  3536. populateReplies(self.server.baseDir, \
  3537. self.server.httpPrefix, \
  3538. self.server.domain, \
  3539. messageJson, \
  3540. self.server.maxReplies, \
  3541. self.server.debug)
  3542. return 1
  3543. else:
  3544. return -1
  3545. elif postType=='newfollowers':
  3546. messageJson= \
  3547. createFollowersOnlyPost(self.server.baseDir, \
  3548. nickname, \
  3549. self.server.domain,self.server.port, \
  3550. self.server.httpPrefix, \
  3551. fields['message'],True,False,False, \
  3552. filename,attachmentMediaType, \
  3553. fields['imageDescription'], \
  3554. self.server.useBlurHash, \
  3555. fields['replyTo'], fields['replyTo'], \
  3556. fields['subject'], \
  3557. fields['eventDate'],fields['eventTime'], \
  3558. fields['location'])
  3559. if messageJson:
  3560. self.postToNickname=nickname
  3561. if self._postToOutbox(messageJson,__version__):
  3562. populateReplies(self.server.baseDir, \
  3563. self.server.httpPrefix, \
  3564. self.server.domain, \
  3565. messageJson, \
  3566. self.server.maxReplies, \
  3567. self.server.debug)
  3568. return 1
  3569. else:
  3570. return -1
  3571. elif postType=='newdm':
  3572. messageJson=None
  3573. if '@' in fields['message']:
  3574. messageJson= \
  3575. createDirectMessagePost(self.server.baseDir, \
  3576. nickname, \
  3577. self.server.domain,self.server.port, \
  3578. self.server.httpPrefix, \
  3579. fields['message'],True,False,False, \
  3580. filename,attachmentMediaType, \
  3581. fields['imageDescription'], \
  3582. self.server.useBlurHash, \
  3583. fields['replyTo'],fields['replyTo'], \
  3584. fields['subject'], \
  3585. self.server.debug, \
  3586. fields['eventDate'], \
  3587. fields['eventTime'], \
  3588. fields['location'])
  3589. if messageJson:
  3590. self.postToNickname=nickname
  3591. if self.server.debug:
  3592. print('DEBUG: new DM to '+str(messageJson['object']['to']))
  3593. if self._postToOutbox(messageJson,__version__):
  3594. populateReplies(self.server.baseDir, \
  3595. self.server.httpPrefix, \
  3596. self.server.domain, \
  3597. messageJson, \
  3598. self.server.maxReplies, \
  3599. self.server.debug)
  3600. return 1
  3601. else:
  3602. return -1
  3603. elif postType=='newreport':
  3604. if attachmentMediaType:
  3605. if attachmentMediaType!='image':
  3606. return -1
  3607. # So as to be sure that this only goes to moderators
  3608. # and not accounts being reported we disable any
  3609. # included fediverse addresses by replacing '@' with '-at-'
  3610. fields['message']=fields['message'].replace('@','-at-')
  3611. messageJson= \
  3612. createReportPost(self.server.baseDir, \
  3613. nickname, \
  3614. self.server.domain,self.server.port, \
  3615. self.server.httpPrefix, \
  3616. fields['message'],True,False,False, \
  3617. filename,attachmentMediaType, \
  3618. fields['imageDescription'], \
  3619. self.server.useBlurHash, \
  3620. self.server.debug,fields['subject'])
  3621. if messageJson:
  3622. self.postToNickname=nickname
  3623. if self._postToOutbox(messageJson,__version__):
  3624. return 1
  3625. else:
  3626. return -1
  3627. elif postType=='newquestion':
  3628. if not fields.get('duration'):
  3629. return -1
  3630. if not fields.get('message'):
  3631. return -1
  3632. questionStr=fields['message']
  3633. qOptions=[]
  3634. for questionCtr in range(8):
  3635. if fields.get('questionOption'+str(questionCtr)):
  3636. qOptions.append(fields['questionOption'+str(questionCtr)])
  3637. if not qOptions:
  3638. return -1
  3639. messageJson= \
  3640. createQuestionPost(self.server.baseDir, \
  3641. nickname, \
  3642. self.server.domain,self.server.port, \
  3643. self.server.httpPrefix, \
  3644. fields['message'],qOptions, \
  3645. False,False,False, \
  3646. filename,attachmentMediaType, \
  3647. fields['imageDescription'], \
  3648. self.server.useBlurHash, \
  3649. fields['subject'],int(fields['duration']))
  3650. if messageJson:
  3651. self.postToNickname=nickname
  3652. if self.server.debug:
  3653. print('DEBUG: new Question')
  3654. if self._postToOutbox(messageJson,__version__):
  3655. return 1
  3656. return -1
  3657. elif postType=='newshare':
  3658. if not fields.get('itemType'):
  3659. return -1
  3660. if not fields.get('category'):
  3661. return -1
  3662. if not fields.get('location'):
  3663. return -1
  3664. if not fields.get('duration'):
  3665. return -1
  3666. if attachmentMediaType:
  3667. if attachmentMediaType!='image':
  3668. return -1
  3669. durationStr=fields['duration']
  3670. if durationStr:
  3671. if ' ' not in durationStr:
  3672. durationStr=durationStr+' days'
  3673. addShare(self.server.baseDir, \
  3674. self.server.httpPrefix, \
  3675. nickname, \
  3676. self.server.domain,self.server.port, \
  3677. fields['subject'], \
  3678. fields['message'], \
  3679. filename, \
  3680. fields['itemType'], \
  3681. fields['category'], \
  3682. fields['location'], \
  3683. durationStr,
  3684. self.server.debug)
  3685. if filename:
  3686. if os.path.isfile(filename):
  3687. os.remove(filename)
  3688. self.postToNickname=nickname
  3689. return 1
  3690. return -1
  3691. def _receiveNewPost(self,authorized: bool,postType: str,path: str) -> int:
  3692. """A new post has been created
  3693. This creates a thread to send the new post
  3694. """
  3695. pageNumber=1
  3696. if not authorized:
  3697. print('Not receiving new post for '+path+' because not authorized')
  3698. return None
  3699. if '/users/' not in path:
  3700. print('Not receiving new post for '+path+' because /users/ not in path')
  3701. return None
  3702. if '?'+postType+'?' not in path:
  3703. print('Not receiving new post for '+path+' because ?'+postType+'? not in path')
  3704. return None
  3705. print('New post begins: '+postType+' '+path)
  3706. if '?page=' in path:
  3707. pageNumberStr=path.split('?page=')[1]
  3708. if '?' in pageNumberStr:
  3709. pageNumberStr=pageNumberStr.split('?')[0]
  3710. if pageNumberStr.isdigit():
  3711. pageNumber=int(pageNumberStr)
  3712. path=path.split('?page=')[0]
  3713. newPostThreadName=self.postToNickname
  3714. if not newPostThreadName:
  3715. newPostThreadName='*'
  3716. if self.server.newPostThread.get(newPostThreadName):
  3717. print('Waiting for previous new post thread to end')
  3718. waitCtr=0
  3719. while self.server.newPostThread[newPostThreadName].isAlive() and waitCtr<8:
  3720. time.sleep(1)
  3721. waitCtr+=1
  3722. if waitCtr>=8:
  3723. self.server.newPostThread[newPostThreadName].kill()
  3724. # make a copy of self.headers
  3725. headers={}
  3726. for dictEntryName,headerLine in self.headers.items():
  3727. headers[dictEntryName]=headerLine
  3728. print('New post headers: '+str(headers))
  3729. length = int(headers['Content-Length'])
  3730. if length>self.server.maxPostLength:
  3731. print('POST size too large')
  3732. return None
  3733. if not headers.get('Content-Type'):
  3734. if headers.get('Content-type'):
  3735. headers['Content-Type']=headers['Content-type']
  3736. elif headers.get('content-type'):
  3737. headers['Content-Type']=headers['content-type']
  3738. if headers.get('Content-Type'):
  3739. if ' boundary=' in headers['Content-Type']:
  3740. boundary=headers['Content-Type'].split('boundary=')[1]
  3741. if ';' in boundary:
  3742. boundary=boundary.split(';')[0]
  3743. postBytes=self.rfile.read(length)
  3744. # second length check from the bytes received
  3745. # since Content-Length could be untruthful
  3746. length=len(postBytes)
  3747. if length>self.server.maxPostLength:
  3748. print('POST size too large')
  3749. return None
  3750. # Note sending new posts needs to be synchronous, otherwise any attachments
  3751. # can get mangled if other events happen during their decoding
  3752. print('Creating new post: '+newPostThreadName)
  3753. self._receiveNewPostProcess(authorized,postType,path,headers,length,postBytes,boundary)
  3754. return pageNumber
  3755. def do_POST(self):
  3756. POSTstartTime=time.time()
  3757. POSTtimings=[]
  3758. if not self.server.session:
  3759. print('Starting new session from POST')
  3760. self.server.session= \
  3761. createSession(self.server.useTor)
  3762. if self.server.debug:
  3763. print('DEBUG: POST to '+self.server.baseDir+ \
  3764. ' path: '+self.path+' busy: '+ \
  3765. str(self.server.POSTbusy))
  3766. if self.server.POSTbusy:
  3767. currTimePOST=int(time.time())
  3768. if currTimePOST-self.server.lastPOST==0:
  3769. self.send_response(429)
  3770. self.end_headers()
  3771. return
  3772. self.server.lastPOST=currTimePOST
  3773. self.server.POSTbusy=True
  3774. if not self.headers.get('Content-type'):
  3775. print('Content-type header missing')
  3776. self.send_response(400)
  3777. self.end_headers()
  3778. self.server.POSTbusy=False
  3779. return
  3780. # remove any trailing slashes from the path
  3781. if not self.path.endswith('confirm'):
  3782. self.path= \
  3783. self.path.replace('/outbox/','/outbox').replace('/inbox/','/inbox').replace('/shares/','/shares').replace('/sharedInbox/','/sharedInbox')
  3784. if self.path=='/inbox':
  3785. if not self.server.enableSharedInbox:
  3786. self._503()
  3787. return
  3788. cookie=None
  3789. if self.headers.get('Cookie'):
  3790. cookie=self.headers['Cookie']
  3791. # check authorization
  3792. authorized = self._isAuthorized()
  3793. if authorized:
  3794. if self.server.debug:
  3795. print('POST Authorization granted')
  3796. else:
  3797. if self.server.debug:
  3798. print('POST Not authorized')
  3799. print(str(self.headers))
  3800. # if this is a POST to teh outbox then check authentication
  3801. self.outboxAuthenticated=False
  3802. self.postToNickname=None
  3803. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,1)
  3804. if self.path.startswith('/login'):
  3805. # get the contents of POST containing login credentials
  3806. length = int(self.headers['Content-length'])
  3807. if length>512:
  3808. print('Login failed - credentials too long')
  3809. self.send_response(401)
  3810. self.end_headers()
  3811. self.server.POSTbusy=False
  3812. return
  3813. loginParams=self.rfile.read(length).decode('utf-8')
  3814. loginNickname,loginPassword,register= \
  3815. htmlGetLoginCredentials(loginParams,self.server.lastLoginTime)
  3816. if loginNickname:
  3817. self.server.lastLoginTime=int(time.time())
  3818. if register:
  3819. if not registerAccount(self.server.baseDir, \
  3820. self.server.httpPrefix, \
  3821. self.server.domain, \
  3822. self.server.port, \
  3823. loginNickname,loginPassword):
  3824. self.server.POSTbusy=False
  3825. self._redirect_headers(self.server.httpPrefix+'://'+self.server.domainFull+'/login',cookie)
  3826. return
  3827. authHeader=createBasicAuthHeader(loginNickname,loginPassword)
  3828. if not authorizeBasic(self.server.baseDir,'/users/'+ \
  3829. loginNickname+'/outbox',authHeader,False):
  3830. print('Login failed: '+loginNickname)
  3831. self._clearLoginDetails(loginNickname)
  3832. self.server.POSTbusy=False
  3833. return
  3834. else:
  3835. if isSuspended(self.server.baseDir,loginNickname):
  3836. msg=htmlSuspended(self.server.baseDir).encode('utf-8')
  3837. self._login_headers('text/html',len(msg))
  3838. self._write(msg)
  3839. self.server.POSTbusy=False
  3840. return
  3841. # login success - redirect with authorization
  3842. print('Login success: '+loginNickname)
  3843. self.send_response(303)
  3844. # re-activate account if needed
  3845. activateAccount(self.server.baseDir,loginNickname,self.server.domain)
  3846. # This produces a deterministic token based on nick+password+salt
  3847. saltFilename= \
  3848. self.server.baseDir+'/accounts/'+ \
  3849. loginNickname+'@'+self.server.domain+'/.salt'
  3850. salt=createPassword(32)
  3851. if os.path.isfile(saltFilename):
  3852. try:
  3853. with open(saltFilename, 'r') as fp:
  3854. salt = fp.read()
  3855. except Exception as e:
  3856. print('WARN: Unable to read salt for '+ \
  3857. loginNickname+' '+str(e))
  3858. else:
  3859. try:
  3860. with open(saltFilename, 'w') as fp:
  3861. fp.write(salt)
  3862. except Exception as e:
  3863. print('WARN: Unable to save salt for '+ \
  3864. loginNickname+' '+str(e))
  3865. token=sha256((loginNickname+loginPassword+salt).encode('utf-8')).hexdigest()
  3866. self.server.tokens[loginNickname]=token
  3867. tokenFilename= \
  3868. self.server.baseDir+'/accounts/'+ \
  3869. loginNickname+'@'+self.server.domain+'/.token'
  3870. try:
  3871. with open(tokenFilename, 'w') as fp:
  3872. fp.write(token)
  3873. except Exception as e:
  3874. print('WARN: Unable to save token for '+loginNickname+' '+str(e))
  3875. self.server.tokensLookup[self.server.tokens[loginNickname]]=loginNickname
  3876. self.send_header('Set-Cookie', \
  3877. 'epicyon='+self.server.tokens[loginNickname]+'; SameSite=Strict')
  3878. self.send_header('Location', \
  3879. self.server.httpPrefix+'://'+ \
  3880. self.server.domainFull+ \
  3881. '/users/'+loginNickname+'/'+self.server.defaultTimeline)
  3882. self.send_header('Content-Length', '0')
  3883. self.send_header('X-Robots-Tag','noindex')
  3884. self.end_headers()
  3885. self.server.POSTbusy=False
  3886. return
  3887. self.send_response(200)
  3888. self.end_headers()
  3889. self.server.POSTbusy=False
  3890. return
  3891. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,2)
  3892. # update of profile/avatar from web interface
  3893. if authorized and self.path.endswith('/profiledata'):
  3894. actorStr= \
  3895. self.server.httpPrefix+'://'+self.server.domainFull+ \
  3896. self.path.replace('/profiledata','').replace('/editprofile','')
  3897. if ' boundary=' in self.headers['Content-type']:
  3898. boundary=self.headers['Content-type'].split('boundary=')[1]
  3899. if ';' in boundary:
  3900. boundary=boundary.split(';')[0]
  3901. nickname=getNicknameFromActor(actorStr)
  3902. if not nickname:
  3903. print('WARN: nickname not found in '+actorStr)
  3904. self._redirect_headers(actorStr,cookie)
  3905. self.server.POSTbusy=False
  3906. return
  3907. length = int(self.headers['Content-length'])
  3908. if length>self.server.maxPostLength:
  3909. print('Maximum profile data length exceeded '+str(length))
  3910. self._redirect_headers(actorStr,cookie)
  3911. self.server.POSTbusy=False
  3912. return
  3913. # read the bytes of the http form POST
  3914. postBytes=self.rfile.read(length)
  3915. # extract each image type
  3916. actorChanged=True
  3917. profileMediaTypes=['avatar','image','banner','instanceLogo']
  3918. profileMediaTypesUploaded={}
  3919. for mType in profileMediaTypes:
  3920. if self.server.debug:
  3921. print('DEBUG: profile update extracting '+mType+' image from POST')
  3922. mediaBytes,postBytes=extractMediaInFormPOST(postBytes,boundary,mType)
  3923. if mediaBytes:
  3924. if self.server.debug:
  3925. print('DEBUG: profile update '+mType+' image was found. '+str(len(mediaBytes))+' bytes')
  3926. else:
  3927. if self.server.debug:
  3928. print('DEBUG: profile update, no '+mType+' image was found in POST')
  3929. continue
  3930. # Note: a .temp extension is used here so that at no time is
  3931. # an image with metadata publicly exposed, even for a few mS
  3932. if mType!='instanceLogo':
  3933. filenameBase= \
  3934. self.server.baseDir+'/accounts/'+ \
  3935. nickname+'@'+self.server.domain+'/'+mType+'.temp'
  3936. else:
  3937. filenameBase= \
  3938. self.server.baseDir+'/accounts/login.temp'
  3939. filename,attachmentMediaType= \
  3940. saveMediaInFormPOST(mediaBytes,self.server.debug,filenameBase)
  3941. if filename:
  3942. if self.server.debug:
  3943. print('DEBUG: profile update POST '+mType+' media filename is '+filename)
  3944. else:
  3945. if self.server.debug:
  3946. print('DEBUG: profile update, no '+mType+' media filename in POST')
  3947. continue
  3948. if self.server.debug:
  3949. print('DEBUG: POST '+mType+' media removing metadata')
  3950. postImageFilename=filename.replace('.temp','')
  3951. removeMetaData(filename,postImageFilename)
  3952. if os.path.isfile(postImageFilename):
  3953. print('profile update POST '+mType+' image saved to '+postImageFilename)
  3954. if mType!='instanceLogo':
  3955. lastPartOfImageFilename=postImageFilename.split('/')[-1]
  3956. profileMediaTypesUploaded[mType]=lastPartOfImageFilename
  3957. actorChanged=True
  3958. else:
  3959. print('ERROR: profile update POST '+mType+' image could not be saved to '+postImageFilename)
  3960. fields=extractTextFieldsInPOST(postBytes,boundary,self.server.debug)
  3961. if self.server.debug:
  3962. if fields:
  3963. print('DEBUG: profile update text field extracted from POST '+str(fields))
  3964. else:
  3965. print('WARN: profile update, no text fields could be extracted from POST')
  3966. actorFilename= \
  3967. self.server.baseDir+'/accounts/'+ \
  3968. nickname+'@'+self.server.domain+'.json'
  3969. if os.path.isfile(actorFilename):
  3970. actorJson=loadJson(actorFilename)
  3971. if actorJson:
  3972. # update the avatar/image url file extension
  3973. for mType,lastPartOfImageFilename in profileMediaTypesUploaded.items():
  3974. if mType=='avatar':
  3975. lastPartOfUrl=actorJson['icon']['url'].split('/')[-1]
  3976. actorJson['icon']['url']= \
  3977. actorJson['icon']['url'].replace('/'+lastPartOfUrl, \
  3978. '/'+lastPartOfImageFilename)
  3979. elif mType=='image':
  3980. lastPartOfUrl=actorJson['image']['url'].split('/')[-1]
  3981. actorJson['image']['url']= \
  3982. actorJson['image']['url'].replace('/'+lastPartOfUrl, \
  3983. '/'+lastPartOfImageFilename)
  3984. skillCtr=1
  3985. newSkills={}
  3986. while skillCtr<10:
  3987. skillName=fields.get('skillName'+str(skillCtr))
  3988. if not skillName:
  3989. skillCtr+=1
  3990. continue
  3991. skillValue=fields.get('skillValue'+str(skillCtr))
  3992. if not skillValue:
  3993. skillCtr+=1
  3994. continue
  3995. if not actorJson['skills'].get(skillName):
  3996. actorChanged=True
  3997. else:
  3998. if actorJson['skills'][skillName]!=int(skillValue):
  3999. actorChanged=True
  4000. newSkills[skillName]=int(skillValue)
  4001. skillCtr+=1
  4002. if len(actorJson['skills'].items())!=len(newSkills.items()):
  4003. actorChanged=True
  4004. actorJson['skills']=newSkills
  4005. if fields.get('password'):
  4006. if fields.get('passwordconfirm'):
  4007. if actorJson['password']==fields['passwordconfirm']:
  4008. if len(actorJson['password'])>2:
  4009. # set password
  4010. storeBasicCredentials(self.server.baseDir,nickname,actorJson['password'])
  4011. if fields.get('displayNickname'):
  4012. if fields['displayNickname']!=actorJson['name']:
  4013. actorJson['name']=fields['displayNickname']
  4014. actorChanged=True
  4015. if fields.get('themeDropdown'):
  4016. setTheme(self.server.baseDir,fields['themeDropdown'])
  4017. #self.server.iconsCache={}
  4018. if fields.get('donateUrl'):
  4019. currentDonateUrl=getDonationUrl(actorJson)
  4020. if fields['donateUrl']!=currentDonateUrl:
  4021. setDonationUrl(actorJson,fields['donateUrl'])
  4022. actorChanged=True
  4023. if fields.get('instanceTitle'):
  4024. currInstanceTitle=getConfigParam(self.server.baseDir,'instanceTitle')
  4025. if fields['instanceTitle']!=currInstanceTitle:
  4026. setConfigParam(self.server.baseDir,'instanceTitle',fields['instanceTitle'])
  4027. if fields.get('instanceDescriptionShort'):
  4028. currInstanceDescriptionShort=getConfigParam(self.server.baseDir,'instanceDescriptionShort')
  4029. if fields['instanceDescriptionShort']!=currInstanceDescriptionShort:
  4030. setConfigParam(self.server.baseDir,'instanceDescriptionShort',fields['instanceDescriptionShort'])
  4031. if fields.get('instanceDescription'):
  4032. currInstanceDescription=getConfigParam(self.server.baseDir,'instanceDescription')
  4033. if fields['instanceDescription']!=currInstanceDescription:
  4034. setConfigParam(self.server.baseDir,'instanceDescription',fields['instanceDescription'])
  4035. if fields.get('bio'):
  4036. if fields['bio']!=actorJson['summary']:
  4037. actorTags={}
  4038. actorJson['summary']= \
  4039. addHtmlTags(self.server.baseDir, \
  4040. self.server.httpPrefix, \
  4041. nickname, \
  4042. self.server.domainFull, \
  4043. fields['bio'],[],actorTags)
  4044. if actorTags:
  4045. actorJson['tag']=[]
  4046. for tagName,tag in actorTags.items():
  4047. actorJson['tag'].append(tag)
  4048. actorChanged=True
  4049. if fields.get('moderators'):
  4050. adminNickname=getConfigParam(self.server.baseDir,'admin')
  4051. if self.path.startswith('/users/'+adminNickname+'/'):
  4052. moderatorsFile=self.server.baseDir+'/accounts/moderators.txt'
  4053. clearModeratorStatus(self.server.baseDir)
  4054. if ',' in fields['moderators']:
  4055. # if the list was given as comma separated
  4056. modFile=open(moderatorsFile,"w+")
  4057. for modNick in fields['moderators'].split(','):
  4058. modNick=modNick.strip()
  4059. if os.path.isdir(self.server.baseDir+ \
  4060. '/accounts/'+modNick+ \
  4061. '@'+self.server.domain):
  4062. modFile.write(modNick+'\n')
  4063. modFile.close()
  4064. for modNick in fields['moderators'].split(','):
  4065. modNick=modNick.strip()
  4066. if os.path.isdir(self.server.baseDir+ \
  4067. '/accounts/'+modNick+ \
  4068. '@'+self.server.domain):
  4069. setRole(self.server.baseDir, \
  4070. modNick,self.server.domain, \
  4071. 'instance','moderator')
  4072. else:
  4073. # nicknames on separate lines
  4074. modFile=open(moderatorsFile,"w+")
  4075. for modNick in fields['moderators'].split('\n'):
  4076. modNick=modNick.strip()
  4077. if os.path.isdir(self.server.baseDir+ \
  4078. '/accounts/'+modNick+ \
  4079. '@'+self.server.domain):
  4080. modFile.write(modNick+'\n')
  4081. modFile.close()
  4082. for modNick in fields['moderators'].split('\n'):
  4083. modNick=modNick.strip()
  4084. if os.path.isdir(self.server.baseDir+ \
  4085. '/accounts/'+modNick+ \
  4086. '@'+self.server.domain):
  4087. setRole(self.server.baseDir, \
  4088. modNick,self.server.domain, \
  4089. 'instance','moderator')
  4090. approveFollowers=False
  4091. if fields.get('approveFollowers'):
  4092. if fields['approveFollowers']=='on':
  4093. approveFollowers=True
  4094. if approveFollowers!=actorJson['manuallyApprovesFollowers']:
  4095. actorJson['manuallyApprovesFollowers']=approveFollowers
  4096. actorChanged=True
  4097. if fields.get('mediaInstance'):
  4098. self.server.mediaInstance=False
  4099. self.server.defaultTimeline='inbox'
  4100. if fields['mediaInstance']=='on':
  4101. self.server.mediaInstance=True
  4102. self.server.defaultTimeline='tlmedia'
  4103. setConfigParam(self.server.baseDir,"mediaInstance", \
  4104. self.server.mediaInstance)
  4105. else:
  4106. if self.server.mediaInstance:
  4107. self.server.mediaInstance=False
  4108. self.server.defaultTimeline='inbox'
  4109. setConfigParam(self.server.baseDir,"mediaInstance", \
  4110. self.server.mediaInstance)
  4111. # only receive DMs from accounts you follow
  4112. followDMsFilename= \
  4113. self.server.baseDir+'/accounts/'+ \
  4114. nickname+'@'+self.server.domain+'/.followDMs'
  4115. followDMsActive=False
  4116. if fields.get('followDMs'):
  4117. if fields['followDMs']=='on':
  4118. followDMsActive=True
  4119. with open(followDMsFilename, "w") as followDMsFile:
  4120. followDMsFile.write('\n')
  4121. if not followDMsActive:
  4122. if os.path.isfile(followDMsFilename):
  4123. os.remove(followDMsFilename)
  4124. # this account is a bot
  4125. if fields.get('isBot'):
  4126. if fields['isBot']=='on':
  4127. if actorJson['type']!='Service':
  4128. actorJson['type']='Service'
  4129. actorChanged=True
  4130. else:
  4131. # this account is a group
  4132. if fields.get('isGroup'):
  4133. if fields['isGroup']=='on':
  4134. if actorJson['type']!='Group':
  4135. actorJson['type']='Group'
  4136. actorChanged=True
  4137. else:
  4138. # this account is a person (default)
  4139. if actorJson['type']!='Person':
  4140. actorJson['type']='Person'
  4141. actorChanged=True
  4142. # save filtered words list
  4143. filterFilename= \
  4144. self.server.baseDir+'/accounts/'+ \
  4145. nickname+'@'+self.server.domain+'/filters.txt'
  4146. if fields.get('filteredWords'):
  4147. with open(filterFilename, "w") as filterfile:
  4148. filterfile.write(fields['filteredWords'])
  4149. else:
  4150. if os.path.isfile(filterFilename):
  4151. os.remove(filterFilename)
  4152. # save blocked accounts list
  4153. blockedFilename= \
  4154. self.server.baseDir+'/accounts/'+ \
  4155. nickname+'@'+self.server.domain+'/blocking.txt'
  4156. if fields.get('blocked'):
  4157. with open(blockedFilename, "w") as blockedfile:
  4158. blockedfile.write(fields['blocked'])
  4159. else:
  4160. if os.path.isfile(blockedFilename):
  4161. os.remove(blockedFilename)
  4162. # save allowed instances list
  4163. allowedInstancesFilename= \
  4164. self.server.baseDir+'/accounts/'+ \
  4165. nickname+'@'+self.server.domain+'/allowedinstances.txt'
  4166. if fields.get('allowedInstances'):
  4167. with open(allowedInstancesFilename, "w") as allowedInstancesFile:
  4168. allowedInstancesFile.write(fields['allowedInstances'])
  4169. else:
  4170. if os.path.isfile(allowedInstancesFilename):
  4171. os.remove(allowedInstancesFilename)
  4172. # save actor json file within accounts
  4173. if actorChanged:
  4174. saveJson(actorJson,actorFilename)
  4175. # also copy to the actors cache and personCache in memory
  4176. storePersonInCache(self.server.baseDir, \
  4177. actorJson['id'],actorJson, \
  4178. self.server.personCache)
  4179. actorCacheFilename= \
  4180. self.server.baseDir+'/cache/actors/'+ \
  4181. actorJson['id'].replace('/','#')+'.json'
  4182. saveJson(actorJson,actorCacheFilename)
  4183. # send actor update to followers
  4184. updateActorJson={
  4185. 'type': 'Update',
  4186. 'actor': actorJson['id'],
  4187. 'to': [actorJson['id']+'/followers'],
  4188. 'cc': [],
  4189. 'object': actorJson
  4190. }
  4191. self.postToNickname=nickname
  4192. self._postToOutbox(updateActorJson,__version__)
  4193. if fields.get('deactivateThisAccount'):
  4194. if fields['deactivateThisAccount']=='on':
  4195. deactivateAccount(self.server.baseDir,nickname,self.server.domain)
  4196. self._clearLoginDetails(nickname)
  4197. self.server.POSTbusy=False
  4198. return
  4199. self._redirect_headers(actorStr,cookie)
  4200. self.server.POSTbusy=False
  4201. return
  4202. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,3)
  4203. # moderator action buttons
  4204. if authorized and '/users/' in self.path and \
  4205. self.path.endswith('/moderationaction'):
  4206. actorStr= \
  4207. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4208. self.path.replace('/moderationaction','')
  4209. length = int(self.headers['Content-length'])
  4210. moderationParams=self.rfile.read(length).decode('utf-8')
  4211. print('moderationParams: '+moderationParams)
  4212. if '&' in moderationParams:
  4213. moderationText=None
  4214. moderationButton=None
  4215. for moderationStr in moderationParams.split('&'):
  4216. print('moderationStr: '+moderationStr)
  4217. if moderationStr.startswith('moderationAction'):
  4218. if '=' in moderationStr:
  4219. moderationText= \
  4220. moderationStr.split('=')[1].strip()
  4221. moderationText= \
  4222. moderationText.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').strip()
  4223. elif moderationStr.startswith('submitInfo'):
  4224. msg=htmlModerationInfo(self.server.translate, \
  4225. self.server.baseDir, \
  4226. self.server.httpPrefix).encode('utf-8')
  4227. self._login_headers('text/html',len(msg))
  4228. self._write(msg)
  4229. self.server.POSTbusy=False
  4230. return
  4231. elif moderationStr.startswith('submitBlock'):
  4232. moderationButton='block'
  4233. elif moderationStr.startswith('submitUnblock'):
  4234. moderationButton='unblock'
  4235. elif moderationStr.startswith('submitSuspend'):
  4236. moderationButton='suspend'
  4237. elif moderationStr.startswith('submitUnsuspend'):
  4238. moderationButton='unsuspend'
  4239. elif moderationStr.startswith('submitRemove'):
  4240. moderationButton='remove'
  4241. if moderationButton and moderationText:
  4242. if self.server.debug:
  4243. print('moderationButton: '+moderationButton)
  4244. print('moderationText: '+moderationText)
  4245. nickname=moderationText
  4246. if nickname.startswith('http') or \
  4247. nickname.startswith('dat'):
  4248. nickname=getNicknameFromActor(nickname)
  4249. if '@' in nickname:
  4250. nickname=nickname.split('@')[0]
  4251. if moderationButton=='suspend':
  4252. suspendAccount(self.server.baseDir,nickname, \
  4253. self.server.domain)
  4254. if moderationButton=='unsuspend':
  4255. unsuspendAccount(self.server.baseDir,nickname)
  4256. if moderationButton=='block':
  4257. fullBlockDomain=None
  4258. if moderationText.startswith('http') or \
  4259. moderationText.startswith('dat'):
  4260. blockDomain,blockPort= \
  4261. getDomainFromActor(moderationText)
  4262. fullBlockDomain=blockDomain
  4263. if blockPort:
  4264. if blockPort!=80 and blockPort!=443:
  4265. if ':' not in blockDomain:
  4266. fullBlockDomain= \
  4267. blockDomain+':'+str(blockPort)
  4268. if '@' in moderationText:
  4269. fullBlockDomain=moderationText.split('@')[1]
  4270. if fullBlockDomain or nickname.startswith('#'):
  4271. addGlobalBlock(self.server.baseDir, \
  4272. nickname,fullBlockDomain)
  4273. if moderationButton=='unblock':
  4274. fullBlockDomain=None
  4275. if moderationText.startswith('http') or \
  4276. moderationText.startswith('dat'):
  4277. blockDomain,blockPort= \
  4278. getDomainFromActor(moderationText)
  4279. fullBlockDomain=blockDomain
  4280. if blockPort:
  4281. if blockPort!=80 and blockPort!=443:
  4282. if ':' not in blockDomain:
  4283. fullBlockDomain= \
  4284. blockDomain+':'+str(blockPort)
  4285. if '@' in moderationText:
  4286. fullBlockDomain=moderationText.split('@')[1]
  4287. if fullBlockDomain or nickname.startswith('#'):
  4288. removeGlobalBlock(self.server.baseDir, \
  4289. nickname,fullBlockDomain)
  4290. if moderationButton=='remove':
  4291. if '/statuses/' not in moderationText:
  4292. removeAccount(self.server.baseDir, \
  4293. nickname, \
  4294. self.server.domain, \
  4295. self.server.port)
  4296. else:
  4297. # remove a post or thread
  4298. postFilename= \
  4299. locatePost(self.server.baseDir, \
  4300. nickname,self.server.domain, \
  4301. moderationText)
  4302. if postFilename:
  4303. if canRemovePost(self.server.baseDir, \
  4304. nickname, \
  4305. self.server.domain, \
  4306. self.server.port, \
  4307. moderationText):
  4308. deletePost(self.server.baseDir, \
  4309. self.server.httpPrefix, \
  4310. nickname,self.server.domain, \
  4311. postFilename, \
  4312. self.server.debug)
  4313. self._redirect_headers(actorStr+'/moderation',cookie)
  4314. self.server.POSTbusy=False
  4315. return
  4316. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,4)
  4317. searchForEmoji=False
  4318. if self.path.endswith('/searchhandleemoji'):
  4319. searchForEmoji=True
  4320. self.path=self.path.replace('/searchhandleemoji','/searchhandle')
  4321. if self.server.debug:
  4322. print('DEBUG: searching for emoji')
  4323. print('authorized: '+str(authorized))
  4324. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,5)
  4325. # a vote/question/poll is posted
  4326. if authorized and \
  4327. (self.path.endswith('/question') or '/question?page=' in self.path):
  4328. pageNumber=1
  4329. if '?page=' in self.path:
  4330. pageNumberStr=self.path.split('?page=')[1]
  4331. if pageNumberStr.isdigit():
  4332. pageNumber=int(pageNumberStr)
  4333. self.path=self.path.split('?page=')[0]
  4334. # the actor who votes
  4335. actor= \
  4336. self.server.httpPrefix+'://'+ \
  4337. self.server.domainFull+self.path.replace('/question','')
  4338. nickname=getNicknameFromActor(actor)
  4339. if not nickname:
  4340. self._redirect_headers(actor+'/'+self.server.defaultTimeline+'?page='+ \
  4341. str(pageNumber),cookie)
  4342. self.server.POSTbusy=False
  4343. return
  4344. # get the parameters
  4345. length = int(self.headers['Content-length'])
  4346. questionParams=self.rfile.read(length).decode('utf-8')
  4347. questionParams= \
  4348. questionParams.replace('+',' ').replace('%40','@').replace('%3A',':').replace('%23','#').replace('%2F','/').strip()
  4349. # post being voted on
  4350. messageId=None
  4351. if 'messageId=' in questionParams:
  4352. messageId=questionParams.split('messageId=')[1]
  4353. if '&' in messageId:
  4354. messageId=messageId.split('&')[0]
  4355. answer=None
  4356. if 'answer=' in questionParams:
  4357. answer=questionParams.split('answer=')[1]
  4358. if '&' in answer:
  4359. answer=answer.split('&')[0]
  4360. self._sendReplyToQuestion(nickname,messageId,answer)
  4361. self._redirect_headers(actor+'/'+self.server.defaultTimeline+ \
  4362. '?page='+str(pageNumber),cookie)
  4363. self.server.POSTbusy=False
  4364. return
  4365. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,6)
  4366. # a search was made
  4367. if (authorized or searchForEmoji) and \
  4368. (self.path.endswith('/searchhandle') or \
  4369. '/searchhandle?page=' in self.path):
  4370. # get the page number
  4371. pageNumber=1
  4372. if '/searchhandle?page=' in self.path:
  4373. pageNumberStr=self.path.split('/searchhandle?page=')[1]
  4374. if pageNumberStr.isdigit():
  4375. pageNumber=int(pageNumberStr)
  4376. self.path=self.path.split('?page=')[0]
  4377. actorStr= \
  4378. self.server.httpPrefix+'://'+ \
  4379. self.server.domainFull+ \
  4380. self.path.replace('/searchhandle','')
  4381. length = int(self.headers['Content-length'])
  4382. searchParams=self.rfile.read(length).decode('utf-8')
  4383. if 'searchtext=' in searchParams:
  4384. searchStr=searchParams.split('searchtext=')[1]
  4385. if '&' in searchStr:
  4386. searchStr=searchStr.split('&')[0]
  4387. searchStr= \
  4388. searchStr.replace('+',' ').replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#')
  4389. searchStr=searchStr.strip()
  4390. if self.server.debug:
  4391. print('searchStr: '+searchStr)
  4392. if searchForEmoji:
  4393. searchStr=':'+searchStr+':'
  4394. if searchStr.startswith('#'):
  4395. # hashtag search
  4396. hashtagStr= \
  4397. htmlHashtagSearch(self.server.domain,self.server.port, \
  4398. self.server.recentPostsCache, \
  4399. self.server.maxRecentPosts, \
  4400. self.server.translate, \
  4401. self.server.baseDir,searchStr[1:],1, \
  4402. maxPostsInFeed,self.server.session, \
  4403. self.server.cachedWebfingers, \
  4404. self.server.personCache, \
  4405. self.server.httpPrefix, \
  4406. self.server.projectVersion)
  4407. if hashtagStr:
  4408. msg=hashtagStr.encode('utf-8')
  4409. self._login_headers('text/html',len(msg))
  4410. self._write(msg)
  4411. self.server.POSTbusy=False
  4412. return
  4413. elif searchStr.startswith('*'):
  4414. # skill search
  4415. searchStr=searchStr.replace('*','').strip()
  4416. skillStr= \
  4417. htmlSkillsSearch(self.server.translate, \
  4418. self.server.baseDir, \
  4419. self.server.httpPrefix, \
  4420. searchStr, \
  4421. self.server.instanceOnlySkillsSearch, \
  4422. 64)
  4423. if skillStr:
  4424. msg=skillStr.encode('utf-8')
  4425. self._login_headers('text/html',len(msg))
  4426. self._write(msg)
  4427. self.server.POSTbusy=False
  4428. return
  4429. elif '@' in searchStr:
  4430. # profile search
  4431. nickname=getNicknameFromActor(self.path)
  4432. if not self.server.session:
  4433. self.server.session= \
  4434. createSession(self.server.useTor)
  4435. profileStr= \
  4436. htmlProfileAfterSearch(self.server.recentPostsCache, \
  4437. self.server.maxRecentPosts, \
  4438. self.server.translate, \
  4439. self.server.baseDir, \
  4440. self.path.replace('/searchhandle',''), \
  4441. self.server.httpPrefix, \
  4442. nickname, \
  4443. self.server.domain,self.server.port, \
  4444. searchStr, \
  4445. self.server.session, \
  4446. self.server.cachedWebfingers, \
  4447. self.server.personCache, \
  4448. self.server.debug, \
  4449. self.server.projectVersion)
  4450. if profileStr:
  4451. msg=profileStr.encode('utf-8')
  4452. self._login_headers('text/html',len(msg))
  4453. self._write(msg)
  4454. self.server.POSTbusy=False
  4455. return
  4456. else:
  4457. self._redirect_headers(actorStr+'/search',cookie)
  4458. self.server.POSTbusy=False
  4459. return
  4460. elif searchStr.startswith(':') or \
  4461. searchStr.lower().strip('\n').endswith(' emoji'):
  4462. # eg. "cat emoji"
  4463. if searchStr.lower().strip('\n').endswith(' emoji'):
  4464. searchStr= \
  4465. searchStr.lower().strip('\n').replace(' emoji','')
  4466. # emoji search
  4467. emojiStr= \
  4468. htmlSearchEmoji(self.server.translate, \
  4469. self.server.baseDir, \
  4470. self.server.httpPrefix, \
  4471. searchStr)
  4472. if emojiStr:
  4473. msg=emojiStr.encode('utf-8')
  4474. self._login_headers('text/html',len(msg))
  4475. self._write(msg)
  4476. self.server.POSTbusy=False
  4477. return
  4478. else:
  4479. # shared items search
  4480. sharedItemsStr= \
  4481. htmlSearchSharedItems(self.server.translate, \
  4482. self.server.baseDir, \
  4483. searchStr,pageNumber, \
  4484. maxPostsInFeed, \
  4485. self.server.httpPrefix, \
  4486. self.server.domainFull, \
  4487. actorStr)
  4488. if sharedItemsStr:
  4489. msg=sharedItemsStr.encode('utf-8')
  4490. self._login_headers('text/html',len(msg))
  4491. self._write(msg)
  4492. self.server.POSTbusy=False
  4493. return
  4494. self._redirect_headers(actorStr+'/'+self.server.defaultTimeline,cookie)
  4495. self.server.POSTbusy=False
  4496. return
  4497. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,7)
  4498. # removes a shared item
  4499. if authorized and self.path.endswith('/rmshare'):
  4500. originPathStr= \
  4501. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4502. self.path.split('/rmshare')[0]
  4503. length = int(self.headers['Content-length'])
  4504. removeShareConfirmParams=self.rfile.read(length).decode('utf-8')
  4505. if '&submitYes=' in removeShareConfirmParams:
  4506. removeShareConfirmParams= \
  4507. removeShareConfirmParams.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').replace('+',' ').strip()
  4508. shareActor=removeShareConfirmParams.split('actor=')[1]
  4509. if '&' in shareActor:
  4510. shareActor=shareActor.split('&')[0]
  4511. shareName=removeShareConfirmParams.split('shareName=')[1]
  4512. if '&' in shareName:
  4513. shareName=shareName.split('&')[0]
  4514. shareNickname=getNicknameFromActor(shareActor)
  4515. if shareNickname:
  4516. shareDomain,sharePort=getDomainFromActor(shareActor)
  4517. removeShare(self.server.baseDir, \
  4518. shareNickname,shareDomain,shareName)
  4519. self._redirect_headers(originPathStr+'/tlshares',cookie)
  4520. self.server.POSTbusy=False
  4521. return
  4522. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,8)
  4523. # removes a post
  4524. if authorized and self.path.endswith('/rmpost'):
  4525. pageNumber=1
  4526. originPathStr= \
  4527. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4528. self.path.split('/rmpost')[0]
  4529. length = int(self.headers['Content-length'])
  4530. removePostConfirmParams=self.rfile.read(length).decode('utf-8')
  4531. if '&submitYes=' in removePostConfirmParams:
  4532. removePostConfirmParams= \
  4533. removePostConfirmParams.replace('%20',' ').replace('%40','@').replace('%3A',':').replace('%2F','/').replace('%23','#').strip()
  4534. removeMessageId= \
  4535. removePostConfirmParams.split('messageId=')[1]
  4536. if '&' in removeMessageId:
  4537. removeMessageId=removeMessageId.split('&')[0]
  4538. if 'pageNumber=' in removePostConfirmParams:
  4539. pageNumberStr=removePostConfirmParams.split('pageNumber=')[1]
  4540. if '&' in pageNumberStr:
  4541. pageNumberStr=pageNumberStr.split('&')[0]
  4542. if pageNumberStr.isdigit():
  4543. pageNumber=int(pageNumberStr)
  4544. if '/statuses/' in removeMessageId:
  4545. removePostActor=removeMessageId.split('/statuses/')[0]
  4546. if originPathStr in removePostActor:
  4547. deleteJson= {
  4548. "@context": "https://www.w3.org/ns/activitystreams",
  4549. 'actor': removePostActor,
  4550. 'object': removeMessageId,
  4551. 'to': ['https://www.w3.org/ns/activitystreams#Public',removePostActor],
  4552. 'cc': [removePostActor+'/followers'],
  4553. 'type': 'Delete'
  4554. }
  4555. self.postToNickname=getNicknameFromActor(removePostActor)
  4556. if self.postToNickname:
  4557. self._postToOutboxThread(deleteJson)
  4558. if pageNumber==1:
  4559. self._redirect_headers(originPathStr+'/outbox',cookie)
  4560. else:
  4561. self._redirect_headers(originPathStr+'/outbox?page='+ \
  4562. str(pageNumber),cookie)
  4563. self.server.POSTbusy=False
  4564. return
  4565. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,9)
  4566. # decision to follow in the web interface is confirmed
  4567. if authorized and self.path.endswith('/followconfirm'):
  4568. originPathStr= \
  4569. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4570. self.path.split('/followconfirm')[0]
  4571. followerNickname=getNicknameFromActor(originPathStr)
  4572. length = int(self.headers['Content-length'])
  4573. followConfirmParams=self.rfile.read(length).decode('utf-8')
  4574. if '&submitView=' in followConfirmParams:
  4575. followingActor= \
  4576. followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  4577. if '&' in followingActor:
  4578. followingActor=followingActor.split('&')[0]
  4579. self._redirect_headers(followingActor,cookie)
  4580. self.server.POSTbusy=False
  4581. return
  4582. if '&submitYes=' in followConfirmParams:
  4583. followingActor= \
  4584. followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  4585. if '&' in followingActor:
  4586. followingActor=followingActor.split('&')[0]
  4587. followingNickname=getNicknameFromActor(followingActor)
  4588. followingDomain,followingPort=getDomainFromActor(followingActor)
  4589. if followerNickname==followingNickname and \
  4590. followingDomain==self.server.domain and \
  4591. followingPort==self.server.port:
  4592. if self.server.debug:
  4593. print('You cannot follow yourself!')
  4594. else:
  4595. if self.server.debug:
  4596. print('Sending follow request from '+ \
  4597. followerNickname+' to '+followingActor)
  4598. sendFollowRequest(self.server.session, \
  4599. self.server.baseDir, \
  4600. followerNickname, \
  4601. self.server.domain,self.server.port, \
  4602. self.server.httpPrefix, \
  4603. followingNickname, \
  4604. followingDomain, \
  4605. followingPort,self.server.httpPrefix, \
  4606. False,self.server.federationList, \
  4607. self.server.sendThreads, \
  4608. self.server.postLog, \
  4609. self.server.cachedWebfingers, \
  4610. self.server.personCache, \
  4611. self.server.debug, \
  4612. self.server.projectVersion)
  4613. self._redirect_headers(originPathStr,cookie)
  4614. self.server.POSTbusy=False
  4615. return
  4616. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,10)
  4617. # decision to unfollow in the web interface is confirmed
  4618. if authorized and self.path.endswith('/unfollowconfirm'):
  4619. originPathStr= \
  4620. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4621. self.path.split('/unfollowconfirm')[0]
  4622. followerNickname=getNicknameFromActor(originPathStr)
  4623. length = int(self.headers['Content-length'])
  4624. followConfirmParams=self.rfile.read(length).decode('utf-8')
  4625. if '&submitYes=' in followConfirmParams:
  4626. followingActor= \
  4627. followConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  4628. if '&' in followingActor:
  4629. followingActor=followingActor.split('&')[0]
  4630. followingNickname=getNicknameFromActor(followingActor)
  4631. followingDomain,followingPort=getDomainFromActor(followingActor)
  4632. if followerNickname==followingNickname and \
  4633. followingDomain==self.server.domain and \
  4634. followingPort==self.server.port:
  4635. if self.server.debug:
  4636. print('You cannot unfollow yourself!')
  4637. else:
  4638. if self.server.debug:
  4639. print(followerNickname+' stops following '+followingActor)
  4640. followActor= \
  4641. self.server.httpPrefix+'://'+ \
  4642. self.server.domainFull+ \
  4643. '/users/'+followerNickname
  4644. statusNumber,published = getStatusNumber()
  4645. followId=followActor+'/statuses/'+str(statusNumber)
  4646. unfollowJson = {
  4647. '@context': 'https://www.w3.org/ns/activitystreams',
  4648. 'id': followId+'/undo',
  4649. 'type': 'Undo',
  4650. 'actor': followActor,
  4651. 'object': {
  4652. 'id': followId,
  4653. 'type': 'Follow',
  4654. 'actor': followActor,
  4655. 'object': followingActor
  4656. }
  4657. }
  4658. pathUsersSection=self.path.split('/users/')[1]
  4659. self.postToNickname=pathUsersSection.split('/')[0]
  4660. self._postToOutboxThread(unfollowJson)
  4661. self._redirect_headers(originPathStr,cookie)
  4662. self.server.POSTbusy=False
  4663. return
  4664. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,11)
  4665. # decision to unblock in the web interface is confirmed
  4666. if authorized and self.path.endswith('/unblockconfirm'):
  4667. originPathStr= \
  4668. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4669. self.path.split('/unblockconfirm')[0]
  4670. blockerNickname=getNicknameFromActor(originPathStr)
  4671. if not blockerNickname:
  4672. print('WARN: unable to find nickname in '+originPathStr)
  4673. self._redirect_headers(originPathStr,cookie)
  4674. self.server.POSTbusy=False
  4675. return
  4676. length = int(self.headers['Content-length'])
  4677. blockConfirmParams=self.rfile.read(length).decode('utf-8')
  4678. if '&submitYes=' in blockConfirmParams:
  4679. blockingActor= \
  4680. blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  4681. if '&' in blockingActor:
  4682. blockingActor=blockingActor.split('&')[0]
  4683. blockingNickname=getNicknameFromActor(blockingActor)
  4684. if not blockingNickname:
  4685. print('WARN: unable to find nickname in '+blockingActor)
  4686. self._redirect_headers(originPathStr,cookie)
  4687. self.server.POSTbusy=False
  4688. return
  4689. blockingDomain,blockingPort=getDomainFromActor(blockingActor)
  4690. blockingDomainFull=blockingDomain
  4691. if blockingPort:
  4692. if blockingPort!=80 and blockingPort!=443:
  4693. if ':' not in blockingDomain:
  4694. blockingDomainFull= \
  4695. blockingDomain+':'+str(blockingPort)
  4696. if blockerNickname==blockingNickname and \
  4697. blockingDomain==self.server.domain and \
  4698. blockingPort==self.server.port:
  4699. if self.server.debug:
  4700. print('You cannot unblock yourself!')
  4701. else:
  4702. if self.server.debug:
  4703. print(blockerNickname+' stops blocking '+blockingActor)
  4704. removeBlock(self.server.baseDir,blockerNickname,self.server.domain, \
  4705. blockingNickname,blockingDomainFull)
  4706. self._redirect_headers(originPathStr,cookie)
  4707. self.server.POSTbusy=False
  4708. return
  4709. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,12)
  4710. # decision to block in the web interface is confirmed
  4711. if authorized and self.path.endswith('/blockconfirm'):
  4712. originPathStr= \
  4713. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4714. self.path.split('/blockconfirm')[0]
  4715. blockerNickname=getNicknameFromActor(originPathStr)
  4716. if not blockerNickname:
  4717. print('WARN: unable to find nickname in '+originPathStr)
  4718. self._redirect_headers(originPathStr,cookie)
  4719. self.server.POSTbusy=False
  4720. return
  4721. length = int(self.headers['Content-length'])
  4722. blockConfirmParams=self.rfile.read(length).decode('utf-8')
  4723. if '&submitYes=' in blockConfirmParams:
  4724. blockingActor= \
  4725. blockConfirmParams.replace('%3A',':').replace('%2F','/').split('actor=')[1]
  4726. if '&' in blockingActor:
  4727. blockingActor=blockingActor.split('&')[0]
  4728. blockingNickname=getNicknameFromActor(blockingActor)
  4729. if not blockingNickname:
  4730. print('WARN: unable to find nickname in '+blockingActor)
  4731. self._redirect_headers(originPathStr,cookie)
  4732. self.server.POSTbusy=False
  4733. return
  4734. blockingDomain,blockingPort= \
  4735. getDomainFromActor(blockingActor)
  4736. blockingDomainFull=blockingDomain
  4737. if blockingPort:
  4738. if blockingPort!=80 and blockingPort!=443:
  4739. if ':' not in blockingDomain:
  4740. blockingDomainFull= \
  4741. blockingDomain+':'+str(blockingPort)
  4742. if blockerNickname==blockingNickname and \
  4743. blockingDomain==self.server.domain and \
  4744. blockingPort==self.server.port:
  4745. if self.server.debug:
  4746. print('You cannot block yourself!')
  4747. else:
  4748. if self.server.debug:
  4749. print('Adding block by '+blockerNickname+ \
  4750. ' of '+blockingActor)
  4751. addBlock(self.server.baseDir,blockerNickname, \
  4752. self.server.domain, \
  4753. blockingNickname,blockingDomainFull)
  4754. self._redirect_headers(originPathStr,cookie)
  4755. self.server.POSTbusy=False
  4756. return
  4757. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,13)
  4758. # an option was chosen from person options screen
  4759. # view/follow/block/report
  4760. if authorized and self.path.endswith('/personoptions'):
  4761. pageNumber=1
  4762. originPathStr= \
  4763. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4764. self.path.split('/personoptions')[0]
  4765. chooserNickname=getNicknameFromActor(originPathStr)
  4766. if not chooserNickname:
  4767. print('WARN: unable to find nickname in '+originPathStr)
  4768. self._redirect_headers(originPathStr,cookie)
  4769. self.server.POSTbusy=False
  4770. return
  4771. length = int(self.headers['Content-length'])
  4772. optionsConfirmParams= \
  4773. self.rfile.read(length).decode('utf-8').replace('%3A',':').replace('%2F','/')
  4774. # page number to return to
  4775. if 'pageNumber=' in optionsConfirmParams:
  4776. pageNumberStr=optionsConfirmParams.split('pageNumber=')[1]
  4777. if '&' in pageNumberStr:
  4778. pageNumberStr=pageNumberStr.split('&')[0]
  4779. if pageNumberStr.isdigit():
  4780. pageNumber=int(pageNumberStr)
  4781. # actor for the person
  4782. optionsActor=optionsConfirmParams.split('actor=')[1]
  4783. if '&' in optionsActor:
  4784. optionsActor=optionsActor.split('&')[0]
  4785. # url of the avatar
  4786. optionsAvatarUrl=optionsConfirmParams.split('avatarUrl=')[1]
  4787. if '&' in optionsAvatarUrl:
  4788. optionsAvatarUrl=optionsAvatarUrl.split('&')[0]
  4789. # link to a post, which can then be included in reports
  4790. postUrl=None
  4791. if 'postUrl' in optionsConfirmParams:
  4792. postUrl=optionsConfirmParams.split('postUrl=')[1]
  4793. if '&' in postUrl:
  4794. postUrl=postUrl.split('&')[0]
  4795. optionsNickname=getNicknameFromActor(optionsActor)
  4796. if not optionsNickname:
  4797. print('WARN: unable to find nickname in '+optionsActor)
  4798. self._redirect_headers(originPathStr,cookie)
  4799. self.server.POSTbusy=False
  4800. return
  4801. optionsDomain,optionsPort=getDomainFromActor(optionsActor)
  4802. optionsDomainFull=optionsDomain
  4803. if optionsPort:
  4804. if optionsPort!=80 and optionsPort!=443:
  4805. if ':' not in optionsDomain:
  4806. optionsDomainFull=optionsDomain+':'+str(optionsPort)
  4807. if chooserNickname==optionsNickname and \
  4808. optionsDomain==self.server.domain and \
  4809. optionsPort==self.server.port:
  4810. if self.server.debug:
  4811. print('You cannot perform an option action on yourself')
  4812. if '&submitView=' in optionsConfirmParams:
  4813. if self.server.debug:
  4814. print('Viewing '+optionsActor)
  4815. self._redirect_headers(optionsActor,cookie)
  4816. self.server.POSTbusy=False
  4817. return
  4818. if '&submitBlock=' in optionsConfirmParams:
  4819. if self.server.debug:
  4820. print('Adding block by '+chooserNickname+ \
  4821. ' of '+optionsActor)
  4822. addBlock(self.server.baseDir,chooserNickname, \
  4823. self.server.domain, \
  4824. optionsNickname,optionsDomainFull)
  4825. if '&submitUnblock=' in optionsConfirmParams:
  4826. if self.server.debug:
  4827. print('Unblocking '+optionsActor)
  4828. msg=htmlUnblockConfirm(self.server.translate, \
  4829. self.server.baseDir, \
  4830. originPathStr, \
  4831. optionsActor, \
  4832. optionsAvatarUrl).encode()
  4833. self._set_headers('text/html',len(msg),cookie)
  4834. self._write(msg)
  4835. self.server.POSTbusy=False
  4836. return
  4837. if '&submitFollow=' in optionsConfirmParams:
  4838. if self.server.debug:
  4839. print('Following '+optionsActor)
  4840. msg=htmlFollowConfirm(self.server.translate, \
  4841. self.server.baseDir, \
  4842. originPathStr, \
  4843. optionsActor, \
  4844. optionsAvatarUrl).encode()
  4845. self._set_headers('text/html',len(msg),cookie)
  4846. self._write(msg)
  4847. self.server.POSTbusy=False
  4848. return
  4849. if '&submitUnfollow=' in optionsConfirmParams:
  4850. if self.server.debug:
  4851. print('Unfollowing '+optionsActor)
  4852. msg=htmlUnfollowConfirm(self.server.translate, \
  4853. self.server.baseDir, \
  4854. originPathStr, \
  4855. optionsActor, \
  4856. optionsAvatarUrl).encode()
  4857. self._set_headers('text/html',len(msg),cookie)
  4858. self._write(msg)
  4859. self.server.POSTbusy=False
  4860. return
  4861. if '&submitDM=' in optionsConfirmParams:
  4862. if self.server.debug:
  4863. print('Sending DM to '+optionsActor)
  4864. reportPath=self.path.replace('/personoptions','')+'/newdm'
  4865. msg=htmlNewPost(False,self.server.translate, \
  4866. self.server.baseDir, \
  4867. self.server.httpPrefix, \
  4868. reportPath,None, \
  4869. [optionsActor],None, \
  4870. pageNumber, \
  4871. chooserNickname,self.server.domain).encode()
  4872. self._set_headers('text/html',len(msg),cookie)
  4873. self._write(msg)
  4874. self.server.POSTbusy=False
  4875. return
  4876. if '&submitSnooze=' in optionsConfirmParams:
  4877. thisActor= \
  4878. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4879. self.path.split('/personoptions')[0]
  4880. if self.server.debug:
  4881. print('Snoozing '+optionsActor+' '+thisActor)
  4882. if '/users/' in thisActor:
  4883. nickname=thisActor.split('/users/')[1]
  4884. personSnooze(self.server.baseDir,nickname,self.server.domain,optionsActor)
  4885. self._redirect_headers(thisActor+ \
  4886. '/'+self.server.defaultTimeline+ \
  4887. '?page='+str(pageNumber),cookie)
  4888. self.server.POSTbusy=False
  4889. return
  4890. if '&submitUnSnooze=' in optionsConfirmParams:
  4891. thisActor= \
  4892. self.server.httpPrefix+'://'+self.server.domainFull+ \
  4893. self.path.split('/personoptions')[0]
  4894. if self.server.debug:
  4895. print('Unsnoozing '+optionsActor+' '+thisActor)
  4896. if '/users/' in thisActor:
  4897. nickname=thisActor.split('/users/')[1]
  4898. personUnsnooze(self.server.baseDir,nickname,self.server.domain,optionsActor)
  4899. self._redirect_headers(thisActor+ \
  4900. '/'+self.server.defaultTimeline+ \
  4901. '?page='+str(pageNumber),cookie)
  4902. self.server.POSTbusy=False
  4903. return
  4904. if '&submitReport=' in optionsConfirmParams:
  4905. if self.server.debug:
  4906. print('Reporting '+optionsActor)
  4907. reportPath=self.path.replace('/personoptions','')+'/newreport'
  4908. msg=htmlNewPost(False,self.server.translate, \
  4909. self.server.baseDir, \
  4910. self.server.httpPrefix, \
  4911. reportPath,None,[], \
  4912. postUrl,pageNumber, \
  4913. chooserNickname,self.server.domain).encode()
  4914. self._set_headers('text/html',len(msg),cookie)
  4915. self._write(msg)
  4916. self.server.POSTbusy=False
  4917. return
  4918. self._redirect_headers(originPathStr,cookie)
  4919. self.server.POSTbusy=False
  4920. return
  4921. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,14)
  4922. # receive different types of post created by htmlNewPost
  4923. postTypes=["newpost","newunlisted","newfollowers","newdm","newreport","newshare","newquestion"]
  4924. for currPostType in postTypes:
  4925. if currPostType!='newshare':
  4926. postRedirect=self.server.defaultTimeline
  4927. else:
  4928. postRedirect='shares'
  4929. pageNumber=self._receiveNewPost(authorized,currPostType,self.path)
  4930. if pageNumber:
  4931. nickname=self.path.split('/users/')[1]
  4932. if '/' in nickname:
  4933. nickname=nickname.split('/')[0]
  4934. self._redirect_headers(self.server.httpPrefix+'://'+self.server.domainFull+ \
  4935. '/users/'+nickname+ \
  4936. '/'+postRedirect+ \
  4937. '?page='+str(pageNumber),cookie)
  4938. self.server.POSTbusy=False
  4939. return
  4940. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,15)
  4941. if self.path.endswith('/outbox') or self.path.endswith('/shares'):
  4942. if '/users/' in self.path:
  4943. if authorized:
  4944. self.outboxAuthenticated=True
  4945. pathUsersSection=self.path.split('/users/')[1]
  4946. self.postToNickname=pathUsersSection.split('/')[0]
  4947. if not self.outboxAuthenticated:
  4948. self.send_response(405)
  4949. self.end_headers()
  4950. self.server.POSTbusy=False
  4951. return
  4952. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,16)
  4953. # check that the post is to an expected path
  4954. if not (self.path.endswith('/outbox') or \
  4955. self.path.endswith('/inbox') or \
  4956. self.path.endswith('/shares') or \
  4957. self.path.endswith('/moderationaction') or \
  4958. self.path.endswith('/caps/new') or \
  4959. self.path=='/sharedInbox'):
  4960. print('Attempt to POST to invalid path '+self.path)
  4961. self.send_response(400)
  4962. self.end_headers()
  4963. self.server.POSTbusy=False
  4964. return
  4965. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,17)
  4966. # read the message and convert it into a python dictionary
  4967. length = int(self.headers['Content-length'])
  4968. if self.server.debug:
  4969. print('DEBUG: content-length: '+str(length))
  4970. if not self.headers['Content-type'].startswith('image/') and \
  4971. not self.headers['Content-type'].startswith('video/') and \
  4972. not self.headers['Content-type'].startswith('audio/'):
  4973. if length>self.server.maxMessageLength:
  4974. print('Maximum message length exceeded '+str(length))
  4975. self.send_response(400)
  4976. self.end_headers()
  4977. self.server.POSTbusy=False
  4978. return
  4979. else:
  4980. if length>self.server.maxMediaSize:
  4981. print('Maximum media size exceeded '+str(length))
  4982. self.send_response(400)
  4983. self.end_headers()
  4984. self.server.POSTbusy=False
  4985. return
  4986. # receive images to the outbox
  4987. if self.headers['Content-type'].startswith('image/') and \
  4988. '/users/' in self.path:
  4989. if not self.outboxAuthenticated:
  4990. if self.server.debug:
  4991. print('DEBUG: unauthenticated attempt to post image to outbox')
  4992. self.send_response(403)
  4993. self.end_headers()
  4994. self.server.POSTbusy=False
  4995. return
  4996. pathUsersSection=self.path.split('/users/')[1]
  4997. if '/' not in pathUsersSection:
  4998. self.send_response(404)
  4999. self.end_headers()
  5000. self.server.POSTbusy=False
  5001. return
  5002. self.postFromNickname=pathUsersSection.split('/')[0]
  5003. accountsDir= \
  5004. self.server.baseDir+'/accounts/'+ \
  5005. self.postFromNickname+'@'+self.server.domain
  5006. if not os.path.isdir(accountsDir):
  5007. self.send_response(404)
  5008. self.end_headers()
  5009. self.server.POSTbusy=False
  5010. return
  5011. mediaBytes=self.rfile.read(length)
  5012. mediaFilenameBase=accountsDir+'/upload'
  5013. mediaFilename=mediaFilenameBase+'.png'
  5014. if self.headers['Content-type'].endswith('jpeg'):
  5015. mediaFilename=mediaFilenameBase+'.jpg'
  5016. if self.headers['Content-type'].endswith('gif'):
  5017. mediaFilename=mediaFilenameBase+'.gif'
  5018. if self.headers['Content-type'].endswith('webp'):
  5019. mediaFilename=mediaFilenameBase+'.webp'
  5020. with open(mediaFilename, 'wb') as avFile:
  5021. avFile.write(mediaBytes)
  5022. if self.server.debug:
  5023. print('DEBUG: image saved to '+mediaFilename)
  5024. self.send_response(201)
  5025. self.end_headers()
  5026. self.server.POSTbusy=False
  5027. return
  5028. # refuse to receive non-json content
  5029. if self.headers['Content-type'] != 'application/json' and \
  5030. self.headers['Content-type'] != 'application/activity+json':
  5031. print("POST is not json: "+self.headers['Content-type'])
  5032. if self.server.debug:
  5033. print(str(self.headers))
  5034. length = int(self.headers['Content-length'])
  5035. if length<self.server.maxPostLength:
  5036. unknownPost=self.rfile.read(length).decode('utf-8')
  5037. print(str(unknownPost))
  5038. self.send_response(400)
  5039. self.end_headers()
  5040. self.server.POSTbusy=False
  5041. return
  5042. if self.server.debug:
  5043. print('DEBUG: Reading message')
  5044. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,18)
  5045. # check content length before reading bytes
  5046. if self.path == '/sharedInbox' or self.path == '/inbox':
  5047. length=0
  5048. if self.headers.get('Content-length'):
  5049. length = int(self.headers['Content-length'])
  5050. elif self.headers.get('Content-Length'):
  5051. length = int(self.headers['Content-Length'])
  5052. elif self.headers.get('content-length'):
  5053. length = int(self.headers['content-length'])
  5054. if length>10240:
  5055. print('WARN: post to shared inbox is too long '+str(length)+' bytes')
  5056. self._400()
  5057. self.server.POSTbusy=False
  5058. return
  5059. messageBytes=self.rfile.read(length)
  5060. # check content length after reading bytes
  5061. if self.path == '/sharedInbox' or self.path == '/inbox':
  5062. lenMessage=len(messageBytes)
  5063. if lenMessage>10240:
  5064. print('WARN: post to shared inbox is too long '+str(lenMessage)+' bytes')
  5065. self._400()
  5066. self.server.POSTbusy=False
  5067. return
  5068. # convert the raw bytes to json
  5069. messageJson=json.loads(messageBytes)
  5070. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,19)
  5071. # https://www.w3.org/TR/activitypub/#object-without-create
  5072. if self.outboxAuthenticated:
  5073. if self._postToOutbox(messageJson,__version__):
  5074. if messageJson.get('id'):
  5075. self.headers['Location']= \
  5076. messageJson['id'].replace('/activity','').replace('/undo','')
  5077. self.send_response(201)
  5078. self.end_headers()
  5079. self.server.POSTbusy=False
  5080. return
  5081. else:
  5082. if self.server.debug:
  5083. print('Failed to post to outbox')
  5084. self.send_response(403)
  5085. self.end_headers()
  5086. self.server.POSTbusy=False
  5087. return
  5088. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,20)
  5089. # check the necessary properties are available
  5090. if self.server.debug:
  5091. print('DEBUG: Check message has params')
  5092. if self.path.endswith('/inbox') or \
  5093. self.path=='/sharedInbox':
  5094. if not inboxMessageHasParams(messageJson):
  5095. if self.server.debug:
  5096. print("DEBUG: inbox message doesn't have the required parameters")
  5097. self.send_response(403)
  5098. self.end_headers()
  5099. self.server.POSTbusy=False
  5100. return
  5101. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,21)
  5102. if not self.headers.get('signature'):
  5103. if 'keyId=' not in self.headers['signature']:
  5104. if self.server.debug:
  5105. print('DEBUG: POST to inbox has no keyId in header signature parameter')
  5106. self.send_response(403)
  5107. self.end_headers()
  5108. self.server.POSTbusy=False
  5109. return
  5110. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,22)
  5111. if not inboxPermittedMessage(self.server.domain, \
  5112. messageJson, \
  5113. self.server.federationList):
  5114. if self.server.debug:
  5115. # https://www.youtube.com/watch?v=K3PrSj9XEu4
  5116. print('DEBUG: Ah Ah Ah')
  5117. self.send_response(403)
  5118. self.end_headers()
  5119. self.server.POSTbusy=False
  5120. return
  5121. self._benchmarkPOSTtimings(POSTstartTime,POSTtimings,23)
  5122. if self.server.debug:
  5123. print('DEBUG: POST saving to inbox queue')
  5124. if '/users/' in self.path:
  5125. pathUsersSection=self.path.split('/users/')[1]
  5126. if '/' not in pathUsersSection:
  5127. if self.server.debug:
  5128. print('DEBUG: This is not a users endpoint')
  5129. else:
  5130. self.postToNickname=pathUsersSection.split('/')[0]
  5131. if self.postToNickname:
  5132. queueStatus= \
  5133. self._updateInboxQueue(self.postToNickname, \
  5134. messageJson,messageBytes)
  5135. if queueStatus==0:
  5136. self.send_response(200)
  5137. self.end_headers()
  5138. self.server.POSTbusy=False
  5139. return
  5140. if queueStatus==1:
  5141. self.send_response(503)
  5142. self.end_headers()
  5143. self.server.POSTbusy=False
  5144. return
  5145. if self.server.debug:
  5146. print('_updateInboxQueue exited without doing anything')
  5147. else:
  5148. if self.server.debug:
  5149. print('self.postToNickname is None')
  5150. self.send_response(403)
  5151. self.end_headers()
  5152. self.server.POSTbusy=False
  5153. return
  5154. else:
  5155. if self.path == '/sharedInbox' or self.path == '/inbox':
  5156. print('DEBUG: POST to shared inbox')
  5157. queueStatus= \
  5158. self._updateInboxQueue('inbox',messageJson,messageBytes)
  5159. if queueStatus==0:
  5160. self.send_response(200)
  5161. self.end_headers()
  5162. self.server.POSTbusy=False
  5163. return
  5164. if queueStatus==1:
  5165. self.send_response(503)
  5166. self.end_headers()
  5167. self.server.POSTbusy=False
  5168. return
  5169. self.send_response(200)
  5170. self.end_headers()
  5171. self.server.POSTbusy=False
  5172. class PubServerUnitTest(PubServer):
  5173. protocol_version = 'HTTP/1.0'
  5174. def runPostsQueue(baseDir: str,sendThreads: [],debug: bool) -> None:
  5175. """Manages the threads used to send posts
  5176. """
  5177. while True:
  5178. time.sleep(1)
  5179. removeDormantThreads(baseDir,sendThreads,debug)
  5180. def runSharesExpire(versionNumber: str,baseDir: str) -> None:
  5181. """Expires shares as needed
  5182. """
  5183. while True:
  5184. time.sleep(120)
  5185. expireShares(baseDir)
  5186. def runPostsWatchdog(projectVersion: str,httpd) -> None:
  5187. """This tries to keep the posts thread running even if it dies
  5188. """
  5189. print('Starting posts queue watchdog')
  5190. postsQueueOriginal=httpd.thrPostsQueue.clone(runPostsQueue)
  5191. httpd.thrPostsQueue.start()
  5192. while True:
  5193. time.sleep(20)
  5194. if not httpd.thrPostsQueue.isAlive():
  5195. httpd.thrPostsQueue.kill()
  5196. httpd.thrPostsQueue=postsQueueOriginal.clone(runPostsQueue)
  5197. httpd.thrPostsQueue.start()
  5198. print('Restarting posts queue...')
  5199. def runSharesExpireWatchdog(projectVersion: str,httpd) -> None:
  5200. """This tries to keep the shares expiry thread running even if it dies
  5201. """
  5202. print('Starting shares expiry watchdog')
  5203. sharesExpireOriginal=httpd.thrSharesExpire.clone(runSharesExpire)
  5204. httpd.thrSharesExpire.start()
  5205. while True:
  5206. time.sleep(20)
  5207. if not httpd.thrSharesExpire.isAlive():
  5208. httpd.thrSharesExpire.kill()
  5209. httpd.thrSharesExpire=sharesExpireOriginal.clone(runSharesExpire)
  5210. httpd.thrSharesExpire.start()
  5211. print('Restarting shares expiry...')
  5212. def loadTokens(baseDir: str,tokensDict: {},tokensLookup: {}) -> None:
  5213. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  5214. for handle in dirs:
  5215. if '@' in handle:
  5216. tokenFilename=baseDir+'/accounts/'+handle+'/.token'
  5217. if not os.path.isfile(tokenFilename):
  5218. continue
  5219. nickname=handle.split('@')[0]
  5220. token=None
  5221. try:
  5222. with open(tokenFilename, 'r') as fp:
  5223. token = fp.read()
  5224. except Exception as e:
  5225. print('WARN: Unable to read token for '+nickname+' '+str(e))
  5226. if not token:
  5227. continue
  5228. tokensDict[nickname]=token
  5229. tokensLookup[token]=nickname
  5230. def runDaemon(mediaInstance: bool,maxRecentPosts: int, \
  5231. enableSharedInbox: bool,registration: bool, \
  5232. language: str,projectVersion: str, \
  5233. instanceId: str,clientToServer: bool, \
  5234. baseDir: str,domain: str, \
  5235. port=80,proxyPort=80,httpPrefix='https', \
  5236. fedList=[],maxMentions=10,maxEmoji=10, \
  5237. authenticatedFetch=False, \
  5238. noreply=False,nolike=False,nopics=False, \
  5239. noannounce=False,cw=False,ocapAlways=False, \
  5240. useTor=False,maxReplies=64, \
  5241. domainMaxPostsPerDay=8640,accountMaxPostsPerDay=8640, \
  5242. allowDeletion=False,debug=False,unitTest=False, \
  5243. instanceOnlySkillsSearch=False,sendThreads=[], \
  5244. useBlurHash=False) -> None:
  5245. if len(domain)==0:
  5246. domain='localhost'
  5247. if '.' not in domain:
  5248. if domain != 'localhost':
  5249. print('Invalid domain: ' + domain)
  5250. return
  5251. serverAddress = ('', proxyPort)
  5252. if unitTest:
  5253. httpd = ThreadingHTTPServer(serverAddress, PubServerUnitTest)
  5254. else:
  5255. httpd = ThreadingHTTPServer(serverAddress, PubServer)
  5256. httpd.useBlurHash=useBlurHash
  5257. httpd.mediaInstance=mediaInstance
  5258. httpd.defaultTimeline='inbox'
  5259. if mediaInstance:
  5260. httpd.defaultTimeline='tlmedia'
  5261. # load translations dictionary
  5262. httpd.translate={}
  5263. httpd.systemLanguage='en'
  5264. if not unitTest:
  5265. if not os.path.isdir(baseDir+'/translations'):
  5266. print('ERROR: translations directory not found')
  5267. return
  5268. if not language:
  5269. systemLanguage=locale.getdefaultlocale()[0]
  5270. else:
  5271. systemLanguage=language
  5272. if not systemLanguage:
  5273. systemLanguage='en'
  5274. if '_' in systemLanguage:
  5275. systemLanguage=systemLanguage.split('_')[0]
  5276. while '/' in systemLanguage:
  5277. systemLanguage=systemLanguage.split('/')[1]
  5278. if '.' in systemLanguage:
  5279. systemLanguage=systemLanguage.split('.')[0]
  5280. translationsFile=baseDir+'/translations/'+systemLanguage+'.json'
  5281. if not os.path.isfile(translationsFile):
  5282. systemLanguage='en'
  5283. translationsFile=baseDir+'/translations/'+systemLanguage+'.json'
  5284. print('System language: '+systemLanguage)
  5285. httpd.systemLanguage=systemLanguage
  5286. httpd.translate=loadJson(translationsFile)
  5287. if registration=='open':
  5288. httpd.registration=True
  5289. else:
  5290. httpd.registration=False
  5291. httpd.enableSharedInbox=enableSharedInbox
  5292. httpd.outboxThread={}
  5293. httpd.newPostThread={}
  5294. httpd.projectVersion=projectVersion
  5295. httpd.authenticatedFetch=authenticatedFetch
  5296. # max POST size of 30M
  5297. httpd.maxPostLength=1024*1024*30
  5298. httpd.maxMediaSize=httpd.maxPostLength
  5299. httpd.maxMessageLength=8000
  5300. httpd.maxPostsInBox=32000
  5301. httpd.domain=domain
  5302. httpd.port=port
  5303. httpd.domainFull=domain
  5304. if port:
  5305. if port!=80 and port!=443:
  5306. if ':' not in domain:
  5307. httpd.domainFull=domain+':'+str(port)
  5308. httpd.httpPrefix=httpPrefix
  5309. httpd.debug=debug
  5310. httpd.federationList=fedList.copy()
  5311. httpd.baseDir=baseDir
  5312. httpd.instanceId=instanceId
  5313. httpd.personCache={}
  5314. httpd.cachedWebfingers={}
  5315. httpd.useTor=useTor
  5316. httpd.session = None
  5317. httpd.sessionLastUpdate=0
  5318. httpd.lastGET=0
  5319. httpd.lastPOST=0
  5320. httpd.GETbusy=False
  5321. httpd.POSTbusy=False
  5322. httpd.receivedMessage=False
  5323. httpd.inboxQueue=[]
  5324. httpd.sendThreads=sendThreads
  5325. httpd.postLog=[]
  5326. httpd.maxQueueLength=16
  5327. httpd.ocapAlways=ocapAlways
  5328. httpd.allowDeletion=allowDeletion
  5329. httpd.lastLoginTime=0
  5330. httpd.maxReplies=maxReplies
  5331. httpd.tokens={}
  5332. httpd.tokensLookup={}
  5333. loadTokens(baseDir,httpd.tokens,httpd.tokensLookup)
  5334. httpd.instanceOnlySkillsSearch=instanceOnlySkillsSearch
  5335. httpd.acceptedCaps=["inbox:write","objects:read"]
  5336. # contains threads used to send posts to followers
  5337. httpd.followersThreads=[]
  5338. if noreply:
  5339. httpd.acceptedCaps.append('inbox:noreply')
  5340. if nolike:
  5341. httpd.acceptedCaps.append('inbox:nolike')
  5342. if nopics:
  5343. httpd.acceptedCaps.append('inbox:nopics')
  5344. if noannounce:
  5345. httpd.acceptedCaps.append('inbox:noannounce')
  5346. if cw:
  5347. httpd.acceptedCaps.append('inbox:cw')
  5348. if not os.path.isdir(baseDir+'/accounts/inbox@'+domain):
  5349. print('Creating shared inbox: inbox@'+domain)
  5350. createSharedInbox(baseDir,'inbox',domain,port,httpPrefix)
  5351. if not os.path.isdir(baseDir+'/cache'):
  5352. os.mkdir(baseDir+'/cache')
  5353. if not os.path.isdir(baseDir+'/cache/actors'):
  5354. print('Creating actors cache')
  5355. os.mkdir(baseDir+'/cache/actors')
  5356. if not os.path.isdir(baseDir+'/cache/announce'):
  5357. print('Creating announce cache')
  5358. os.mkdir(baseDir+'/cache/announce')
  5359. if not os.path.isdir(baseDir+'/cache/avatars'):
  5360. print('Creating avatars cache')
  5361. os.mkdir(baseDir+'/cache/avatars')
  5362. archiveDir=baseDir+'/archive'
  5363. if not os.path.isdir(archiveDir):
  5364. print('Creating archive')
  5365. os.mkdir(archiveDir)
  5366. print('Creating cache expiry thread')
  5367. httpd.thrCache= \
  5368. threadWithTrace(target=expireCache, \
  5369. args=(baseDir,httpd.personCache, \
  5370. httpd.httpPrefix, \
  5371. archiveDir, \
  5372. httpd.maxPostsInBox),daemon=True)
  5373. httpd.thrCache.start()
  5374. print('Creating posts queue')
  5375. httpd.thrPostsQueue= \
  5376. threadWithTrace(target=runPostsQueue, \
  5377. args=(baseDir,httpd.sendThreads,debug),daemon=True)
  5378. if not unitTest:
  5379. httpd.thrPostsWatchdog= \
  5380. threadWithTrace(target=runPostsWatchdog, \
  5381. args=(projectVersion,httpd),daemon=True)
  5382. httpd.thrPostsWatchdog.start()
  5383. else:
  5384. httpd.thrPostsQueue.start()
  5385. print('Creating expire thread for shared items')
  5386. httpd.thrSharesExpire= \
  5387. threadWithTrace(target=runSharesExpire, \
  5388. args=(__version__,baseDir),daemon=True)
  5389. if not unitTest:
  5390. httpd.thrSharesExpireWatchdog= \
  5391. threadWithTrace(target=runSharesExpireWatchdog, \
  5392. args=(projectVersion,httpd),daemon=True)
  5393. httpd.thrSharesExpireWatchdog.start()
  5394. else:
  5395. httpd.thrSharesExpire.start()
  5396. httpd.recentPostsCache={}
  5397. httpd.maxRecentPosts=maxRecentPosts
  5398. httpd.iconsCache={}
  5399. print('Creating inbox queue')
  5400. httpd.thrInboxQueue= \
  5401. threadWithTrace(target=runInboxQueue, \
  5402. args=(httpd.recentPostsCache,httpd.maxRecentPosts, \
  5403. projectVersion, \
  5404. baseDir,httpPrefix,httpd.sendThreads, \
  5405. httpd.postLog,httpd.cachedWebfingers, \
  5406. httpd.personCache,httpd.inboxQueue, \
  5407. domain,port,useTor,httpd.federationList, \
  5408. httpd.ocapAlways,maxReplies, \
  5409. domainMaxPostsPerDay,accountMaxPostsPerDay, \
  5410. allowDeletion,debug,maxMentions,maxEmoji, \
  5411. httpd.translate, \
  5412. unitTest,httpd.acceptedCaps),daemon=True)
  5413. if not unitTest:
  5414. httpd.thrWatchdog= \
  5415. threadWithTrace(target=runInboxQueueWatchdog, \
  5416. args=(projectVersion,httpd),daemon=True)
  5417. httpd.thrWatchdog.start()
  5418. else:
  5419. httpd.thrInboxQueue.start()
  5420. if clientToServer:
  5421. print('Running ActivityPub client on ' + domain + ' port ' + str(proxyPort))
  5422. else:
  5423. print('Running ActivityPub server on ' + domain + ' port ' + str(proxyPort))
  5424. httpd.serve_forever()