daemon.py 252 KB

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