daemon.py 275 KB

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