webinterface.py 198 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103
  1. __filename__ = "webinterface.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.1.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import json
  9. import time
  10. import os
  11. from collections import OrderedDict
  12. from datetime import datetime
  13. from datetime import date
  14. from dateutil.parser import parse
  15. from shutil import copyfile
  16. from shutil import copyfileobj
  17. from pprint import pprint
  18. from person import personBoxJson
  19. from person import isPersonSnoozed
  20. from pgp import getEmailAddress
  21. from pgp import getPGPpubKey
  22. from xmpp import getXmppAddress
  23. from matrix import getMatrixAddress
  24. from donate import getDonationUrl
  25. from utils import updateRecentPostsCache
  26. from utils import getNicknameFromActor
  27. from utils import getDomainFromActor
  28. from utils import locatePost
  29. from utils import noOfAccounts
  30. from utils import isPublicPost
  31. from utils import isPublicPostFromUrl
  32. from utils import getDisplayName
  33. from utils import getCachedPostDirectory
  34. from utils import getCachedPostFilename
  35. from utils import loadJson
  36. from utils import saveJson
  37. from follow import isFollowingActor
  38. from webfinger import webfingerHandle
  39. from posts import isDM
  40. from posts import getPersonBox
  41. from posts import getUserUrl
  42. from posts import parseUserFeed
  43. from posts import populateRepliesJson
  44. from posts import isModerator
  45. from posts import outboxMessageCreateWrap
  46. from posts import downloadAnnounce
  47. from session import getJson
  48. from auth import createPassword
  49. from like import likedByPerson
  50. from like import noOfLikes
  51. from bookmarks import bookmarkedByPerson
  52. from announce import announcedByPerson
  53. from blocking import isBlocked
  54. from blocking import isBlockedHashtag
  55. from content import getMentionsFromHtml
  56. from content import addHtmlTags
  57. from content import replaceEmojiFromTags
  58. from content import removeLongWords
  59. from config import getConfigParam
  60. from skills import getSkills
  61. from cache import getPersonFromCache
  62. from cache import storePersonInCache
  63. from shares import getValidSharedItemID
  64. def updateAvatarImageCache(session,baseDir: str,httpPrefix: str,actor: str,avatarUrl: str,personCache: {},force=False) -> str:
  65. """Updates the cached avatar for the given actor
  66. """
  67. if not avatarUrl:
  68. return None
  69. actorStr=actor.replace('/','-')
  70. avatarImagePath=baseDir+'/cache/avatars/'+actorStr
  71. if avatarUrl.endswith('.png') or '.png?' in avatarUrl:
  72. sessionHeaders = {'Accept': 'image/png'}
  73. avatarImageFilename=avatarImagePath+'.png'
  74. elif avatarUrl.endswith('.jpg') or avatarUrl.endswith('.jpeg') or \
  75. '.jpg?' in avatarUrl or '.jpeg?' in avatarUrl:
  76. sessionHeaders = {'Accept': 'image/jpeg'}
  77. avatarImageFilename=avatarImagePath+'.jpg'
  78. elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl:
  79. sessionHeaders = {'Accept': 'image/gif'}
  80. avatarImageFilename=avatarImagePath+'.gif'
  81. elif avatarUrl.endswith('.webp') or '.webp?' in avatarUrl:
  82. sessionHeaders = {'Accept': 'image/webp'}
  83. avatarImageFilename=avatarImagePath+'.webp'
  84. else:
  85. return None
  86. if not os.path.isfile(avatarImageFilename) or force:
  87. try:
  88. print('avatar image url: '+avatarUrl)
  89. result=session.get(avatarUrl, headers=sessionHeaders, params=None)
  90. if result.status_code<200 or result.status_code>202:
  91. print('Avatar image download failed with status '+str(result.status_code))
  92. # remove partial download
  93. if os.path.isfile(avatarImageFilename):
  94. os.remove(avatarImageFilename)
  95. else:
  96. with open(avatarImageFilename, 'wb') as f:
  97. f.write(result.content)
  98. print('avatar image downloaded for '+actor)
  99. return avatarImageFilename.replace(baseDir+'/cache','')
  100. except Exception as e:
  101. print('Failed to download avatar image: '+str(avatarUrl))
  102. print(e)
  103. if '/channel/' not in actor:
  104. sessionHeaders = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
  105. else:
  106. sessionHeaders = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  107. personJson = getJson(session,actor,sessionHeaders,None,__version__,httpPrefix,None)
  108. if personJson:
  109. if not personJson.get('id'):
  110. return None
  111. if not personJson.get('publicKey'):
  112. return None
  113. if not personJson['publicKey'].get('publicKeyPem'):
  114. return None
  115. if personJson['id']!=actor:
  116. return None
  117. if not personCache.get(actor):
  118. return None
  119. if personCache[actor]['actor']['publicKey']['publicKeyPem']!=personJson['publicKey']['publicKeyPem']:
  120. print("ERROR: public keys don't match when downloading actor for "+actor)
  121. return None
  122. storePersonInCache(baseDir,actor,personJson,personCache)
  123. return getPersonAvatarUrl(baseDir,actor,personCache)
  124. return None
  125. return avatarImageFilename.replace(baseDir+'/cache','')
  126. def getPersonAvatarUrl(baseDir: str,personUrl: str,personCache: {}) -> str:
  127. """Returns the avatar url for the person
  128. """
  129. personJson = getPersonFromCache(baseDir,personUrl,personCache)
  130. if not personJson:
  131. return None
  132. # get from locally stored image
  133. actorStr=personJson['id'].replace('/','-')
  134. avatarImagePath=baseDir+'/cache/avatars/'+actorStr
  135. if os.path.isfile(avatarImagePath+'.png'):
  136. return '/avatars/'+actorStr+'.png'
  137. if os.path.isfile(avatarImagePath+'.jpg'):
  138. return '/avatars/'+actorStr+'.jpg'
  139. if os.path.isfile(avatarImagePath+'.gif'):
  140. return '/avatars/'+actorStr+'.gif'
  141. if os.path.isfile(avatarImagePath+'.webp'):
  142. return '/avatars/'+actorStr+'.webp'
  143. if os.path.isfile(avatarImagePath):
  144. return '/avatars/'+actorStr
  145. if personJson.get('icon'):
  146. if personJson['icon'].get('url'):
  147. return personJson['icon']['url']
  148. return None
  149. def htmlSearchEmoji(translate: {},baseDir: str,httpPrefix: str,searchStr: str) -> str:
  150. """Search results for emoji
  151. """
  152. # emoji.json is generated so that it can be customized and the changes
  153. # will be retained even if default_emoji.json is subsequently updated
  154. if not os.path.isfile(baseDir+'/emoji/emoji.json'):
  155. copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
  156. searchStr=searchStr.lower().replace(':','').strip('\n')
  157. cssFilename=baseDir+'/epicyon-profile.css'
  158. if os.path.isfile(baseDir+'/epicyon.css'):
  159. cssFilename=baseDir+'/epicyon.css'
  160. with open(cssFilename, 'r') as cssFile:
  161. emojiCSS=cssFile.read()
  162. if httpPrefix!='https':
  163. emojiCSS=emojiCSS.replace('https://',httpPrefix+'://')
  164. emojiLookupFilename=baseDir+'/emoji/emoji.json'
  165. # create header
  166. emojiForm=htmlHeader(cssFilename,emojiCSS)
  167. emojiForm+='<center><h1>'+translate['Emoji Search']+'</h1></center>'
  168. # does the lookup file exist?
  169. if not os.path.isfile(emojiLookupFilename):
  170. emojiForm+='<center><h5>'+translate['No results']+'</h5></center>'
  171. emojiForm+=htmlFooter()
  172. return emojiForm
  173. emojiJson=loadJson(emojiLookupFilename)
  174. if emojiJson:
  175. results={}
  176. for emojiName,filename in emojiJson.items():
  177. if searchStr in emojiName:
  178. results[emojiName] = filename+'.png'
  179. for emojiName,filename in emojiJson.items():
  180. if emojiName in searchStr:
  181. results[emojiName] = filename+'.png'
  182. headingShown=False
  183. emojiForm+='<center>'
  184. for emojiName,filename in results.items():
  185. if os.path.isfile(baseDir+'/emoji/'+filename):
  186. if not headingShown:
  187. emojiForm+='<center><h5>'+translate['Copy the text then paste it into your post']+'</h5></center>'
  188. headingShown=True
  189. emojiForm+='<h3>:'+emojiName+':<img loading="lazy" class="searchEmoji" src="/emoji/'+filename+'"/></h3>'
  190. emojiForm+='</center>'
  191. emojiForm+=htmlFooter()
  192. return emojiForm
  193. def getIconsDir(baseDir: str) -> str:
  194. """Returns the directory where icons exist
  195. """
  196. iconsDir='icons'
  197. theme=getConfigParam(baseDir,'theme')
  198. if theme:
  199. if os.path.isdir(baseDir+'/img/icons/'+theme):
  200. iconsDir='icons/'+theme
  201. return iconsDir
  202. def htmlSearchSharedItems(translate: {}, \
  203. baseDir: str,searchStr: str, \
  204. pageNumber: int, \
  205. resultsPerPage: int, \
  206. httpPrefix: str, \
  207. domainFull: str,actor: str) -> str:
  208. """Search results for shared items
  209. """
  210. iconsDir=getIconsDir(baseDir)
  211. currPage=1
  212. ctr=0
  213. sharedItemsForm=''
  214. searchStrLower=searchStr.replace('%2B','+').replace('%40','@').replace('%3A',':').replace('%23','#').lower().strip('\n')
  215. searchStrLowerList=searchStrLower.split('+')
  216. cssFilename=baseDir+'/epicyon-profile.css'
  217. if os.path.isfile(baseDir+'/epicyon.css'):
  218. cssFilename=baseDir+'/epicyon.css'
  219. with open(cssFilename, 'r') as cssFile:
  220. sharedItemsCSS=cssFile.read()
  221. if httpPrefix!='https':
  222. sharedItemsCSS=sharedItemsCSS.replace('https://',httpPrefix+'://')
  223. sharedItemsForm=htmlHeader(cssFilename,sharedItemsCSS)
  224. sharedItemsForm+='<center><h1>'+translate['Shared Items Search']+'</h1></center>'
  225. resultsExist=False
  226. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  227. for handle in dirs:
  228. if '@' not in handle:
  229. continue
  230. contactNickname=handle.split('@')[0]
  231. sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
  232. if not os.path.isfile(sharesFilename):
  233. continue
  234. sharesJson=loadJson(sharesFilename)
  235. if not sharesJson:
  236. continue
  237. for name,sharedItem in sharesJson.items():
  238. matched=True
  239. for searchSubstr in searchStrLowerList:
  240. subStrMatched=False
  241. searchSubstr=searchSubstr.strip()
  242. if searchSubstr in sharedItem['location'].lower():
  243. subStrMatched=True
  244. elif searchSubstr in sharedItem['summary'].lower():
  245. subStrMatched=True
  246. elif searchSubstr in sharedItem['displayName'].lower():
  247. subStrMatched=True
  248. elif searchSubstr in sharedItem['category'].lower():
  249. subStrMatched=True
  250. if not subStrMatched:
  251. matched=False
  252. break
  253. if matched:
  254. if currPage==pageNumber:
  255. sharedItemsForm+='<div class="container">'
  256. sharedItemsForm+='<p class="share-title">'+sharedItem['displayName']+'</p>'
  257. if sharedItem.get('imageUrl'):
  258. sharedItemsForm+='<a href="'+sharedItem['imageUrl']+'">'
  259. sharedItemsForm+='<img loading="lazy" src="'+sharedItem['imageUrl']+'" alt="Item image"></a>'
  260. sharedItemsForm+='<p>'+sharedItem['summary']+'</p>'
  261. sharedItemsForm+='<p><b>'+translate['Type']+':</b> '+sharedItem['itemType']+' '
  262. sharedItemsForm+='<b>'+translate['Category']+':</b> '+sharedItem['category']+' '
  263. sharedItemsForm+='<b>'+translate['Location']+':</b> '+sharedItem['location']+'</p>'
  264. contactActor=httpPrefix+'://'+domainFull+'/users/'+contactNickname
  265. sharedItemsForm+='<p><a href="'+actor+'?replydm=sharedesc:'+sharedItem['displayName']+'?mention='+contactActor+'"><button class="button">'+translate['Contact']+'</button></a>'
  266. if actor.endswith('/users/'+contactNickname):
  267. sharedItemsForm+=' <a href="'+actor+'?rmshare='+name+'"><button class="button">'+translate['Remove']+'</button></a>'
  268. sharedItemsForm+='</p></div>'
  269. if not resultsExist and currPage>1:
  270. # previous page link, needs to be a POST
  271. sharedItemsForm+='<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber-1)+'">'
  272. sharedItemsForm+=' <input type="hidden" name="actor" value="'+actor+'">'
  273. sharedItemsForm+=' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>'
  274. sharedItemsForm+=' <center><a href="'+actor+'" type="submit" name="submitSearch">'
  275. sharedItemsForm+=' <img loading="lazy" class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"/></a>'
  276. sharedItemsForm+=' </center>'
  277. sharedItemsForm+='</form>'
  278. resultsExist=True
  279. ctr+=1
  280. if ctr>=resultsPerPage:
  281. currPage+=1
  282. if currPage>pageNumber:
  283. # next page link, needs to be a POST
  284. sharedItemsForm+='<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber+1)+'">'
  285. sharedItemsForm+=' <input type="hidden" name="actor" value="'+actor+'">'
  286. sharedItemsForm+=' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>'
  287. sharedItemsForm+=' <center><a href="'+actor+'" type="submit" name="submitSearch">'
  288. sharedItemsForm+=' <img loading="lazy" class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"/></a>'
  289. sharedItemsForm+=' </center>'
  290. sharedItemsForm+='</form>'
  291. break
  292. ctr=0
  293. if not resultsExist:
  294. sharedItemsForm+='<center><h5>'+translate['No results']+'</h5></center>'
  295. sharedItemsForm+=htmlFooter()
  296. return sharedItemsForm
  297. def htmlModerationInfo(translate: {},baseDir: str,httpPrefix: str) -> str:
  298. infoForm=''
  299. cssFilename=baseDir+'/epicyon-profile.css'
  300. if os.path.isfile(baseDir+'/epicyon.css'):
  301. cssFilename=baseDir+'/epicyon.css'
  302. with open(cssFilename, 'r') as cssFile:
  303. infoCSS=cssFile.read()
  304. if httpPrefix!='https':
  305. infoCSS=infoCSS.replace('https://',httpPrefix+'://')
  306. infoForm=htmlHeader(cssFilename,infoCSS)
  307. infoForm+='<center><h1>'+translate['Moderation Information']+'</h1></center>'
  308. infoShown=False
  309. suspendedFilename=baseDir+'/accounts/suspended.txt'
  310. if os.path.isfile(suspendedFilename):
  311. with open(suspendedFilename, "r") as f:
  312. suspendedStr = f.read()
  313. infoForm+='<div class="container">'
  314. infoForm+=' <br><b>'+translate['Suspended accounts']+'</b>'
  315. infoForm+=' <br>'+translate['These are currently suspended']
  316. infoForm+=' <textarea id="message" name="suspended" style="height:200px">'+suspendedStr+'</textarea>'
  317. infoForm+='</div>'
  318. infoShown=True
  319. blockingFilename=baseDir+'/accounts/blocking.txt'
  320. if os.path.isfile(blockingFilename):
  321. with open(blockingFilename, "r") as f:
  322. blockedStr = f.read()
  323. infoForm+='<div class="container">'
  324. infoForm+=' <br><b>'+translate['Blocked accounts and hashtags']+'</b>'
  325. infoForm+=' <br>'+translate['These are globally blocked for all accounts on this instance']
  326. infoForm+=' <textarea id="message" name="blocked" style="height:400px">'+blockedStr+'</textarea>'
  327. infoForm+='</div>'
  328. infoShown=True
  329. if not infoShown:
  330. infoForm+='<center><p>'+translate['Any blocks or suspensions made by moderators will be shown here.']+'</p></center>'
  331. infoForm+=htmlFooter()
  332. return infoForm
  333. def htmlHashtagSearch(nickname: str,domain: str,port: int, \
  334. recentPostsCache: {},maxRecentPosts: int, \
  335. translate: {}, \
  336. baseDir: str,hashtag: str,pageNumber: int, \
  337. postsPerPage: int, \
  338. session,wfRequest: {},personCache: {}, \
  339. httpPrefix: str,projectVersion: str) -> str:
  340. """Show a page containing search results for a hashtag
  341. """
  342. iconsDir=getIconsDir(baseDir)
  343. if hashtag.startswith('#'):
  344. hashtag=hashtag[1:]
  345. hashtagIndexFile=baseDir+'/tags/'+hashtag+'.txt'
  346. if not os.path.isfile(hashtagIndexFile):
  347. return None
  348. # check that the directory for the nickname exists
  349. if nickname:
  350. if not os.path.isdir(baseDir+'/accounts/'+nickname+'@'+domain):
  351. nickname=None
  352. # read the index
  353. with open(hashtagIndexFile, "r") as f:
  354. lines = f.readlines()
  355. # read the css
  356. cssFilename=baseDir+'/epicyon-profile.css'
  357. if os.path.isfile(baseDir+'/epicyon.css'):
  358. cssFilename=baseDir+'/epicyon.css'
  359. with open(cssFilename, 'r') as cssFile:
  360. hashtagSearchCSS = cssFile.read()
  361. if httpPrefix!='https':
  362. hashtagSearchCSS=hashtagSearchCSS.replace('https://',httpPrefix+'://')
  363. # ensure that the page number is in bounds
  364. if not pageNumber:
  365. pageNumber=1
  366. elif pageNumber<1:
  367. pageNumber=1
  368. # get the start end end within the index file
  369. startIndex=int((pageNumber-1)*postsPerPage)
  370. endIndex=startIndex+postsPerPage
  371. noOfLines=len(lines)
  372. if endIndex>=noOfLines and noOfLines>0:
  373. endIndex=noOfLines-1
  374. # add the page title
  375. hashtagSearchForm=htmlHeader(cssFilename,hashtagSearchCSS)
  376. hashtagSearchForm+='<script>'+contentWarningScript()+'</script>'
  377. hashtagSearchForm+='<center><h1>#'+hashtag+'</h1></center>'
  378. if startIndex>0:
  379. # previous page link
  380. hashtagSearchForm+= \
  381. '<center><a href="/tags/'+hashtag+'?page='+ \
  382. str(pageNumber-1)+'"><img loading="lazy" class="pageicon" src="/'+ \
  383. iconsDir+'/pageup.png" title="'+translate['Page up']+ \
  384. '" alt="'+translate['Page up']+'"></a></center>'
  385. index=startIndex
  386. while index<=endIndex:
  387. postId=lines[index].strip('\n')
  388. if ' ' not in postId:
  389. nickname=getNicknameFromActor(postId)
  390. if not nickname:
  391. index+=1
  392. continue
  393. else:
  394. postFields=postId.split(' ')
  395. if len(postFields)!=3:
  396. index=+1
  397. continue
  398. postDaysSinceEposh=int(postFields[0])
  399. nickname=postFields[1]
  400. postId=postFields[2]
  401. postFilename=locatePost(baseDir,nickname,domain,postId)
  402. if not postFilename:
  403. index+=1
  404. continue
  405. postJsonObject=loadJson(postFilename)
  406. if postJsonObject:
  407. if not isPublicPost(postJsonObject):
  408. index+=1
  409. continue
  410. showIndividualPostIcons=False
  411. if nickname:
  412. showIndividualPostIcons=True
  413. allowDeletion=False
  414. hashtagSearchForm+= \
  415. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  416. iconsDir,translate,None, \
  417. baseDir,session,wfRequest,personCache, \
  418. nickname,domain,port,postJsonObject, \
  419. None,True,allowDeletion, \
  420. httpPrefix,projectVersion,'search', \
  421. showIndividualPostIcons, \
  422. showIndividualPostIcons, \
  423. False,False,False)
  424. index+=1
  425. if endIndex<noOfLines-1:
  426. # next page link
  427. hashtagSearchForm+= \
  428. '<center><a href="/tags/'+hashtag+'?page='+str(pageNumber+1)+ \
  429. '"><img loading="lazy" class="pageicon" src="/'+iconsDir+ \
  430. '/pagedown.png" title="'+translate['Page down']+ \
  431. '" alt="'+translate['Page down']+'"></a></center>'
  432. hashtagSearchForm+=htmlFooter()
  433. return hashtagSearchForm
  434. def htmlSkillsSearch(translate: {},baseDir: str, \
  435. httpPrefix: str, \
  436. skillsearch: str,instanceOnly: bool, \
  437. postsPerPage: int) -> str:
  438. """Show a page containing search results for a skill
  439. """
  440. if skillsearch.startswith('*'):
  441. skillsearch=skillsearch[1:].strip()
  442. skillsearch=skillsearch.lower().strip('\n')
  443. results=[]
  444. # search instance accounts
  445. for subdir, dirs, files in os.walk(baseDir+'/accounts/'):
  446. for f in files:
  447. if not f.endswith('.json'):
  448. continue
  449. if '@' not in f:
  450. continue
  451. if f.startswith('inbox@'):
  452. continue
  453. actorFilename = os.path.join(subdir, f)
  454. actorJson=loadJson(actorFilename)
  455. if actorJson:
  456. if actorJson.get('id') and \
  457. actorJson.get('skills') and \
  458. actorJson.get('name') and \
  459. actorJson.get('icon'):
  460. actor=actorJson['id']
  461. for skillName,skillLevel in actorJson['skills'].items():
  462. skillName=skillName.lower()
  463. if skillName in skillsearch or skillsearch in skillName:
  464. skillLevelStr=str(skillLevel)
  465. if skillLevel<100:
  466. skillLevelStr='0'+skillLevelStr
  467. if skillLevel<10:
  468. skillLevelStr='0'+skillLevelStr
  469. indexStr=skillLevelStr+';'+actor+';'+actorJson['name']+';'+actorJson['icon']['url']
  470. if indexStr not in results:
  471. results.append(indexStr)
  472. if not instanceOnly:
  473. # search actor cache
  474. for subdir, dirs, files in os.walk(baseDir+'/cache/actors/'):
  475. for f in files:
  476. if not f.endswith('.json'):
  477. continue
  478. if '@' not in f:
  479. continue
  480. if f.startswith('inbox@'):
  481. continue
  482. actorFilename = os.path.join(subdir, f)
  483. cachedActorJson=loadJson(actorFilename)
  484. if cachedActorJson:
  485. if cachedActorJson.get('actor'):
  486. actorJson=cachedActorJson['actor']
  487. if actorJson.get('id') and \
  488. actorJson.get('skills') and \
  489. actorJson.get('name') and \
  490. actorJson.get('icon'):
  491. actor=actorJson['id']
  492. for skillName,skillLevel in actorJson['skills'].items():
  493. skillName=skillName.lower()
  494. if skillName in skillsearch or skillsearch in skillName:
  495. skillLevelStr=str(skillLevel)
  496. if skillLevel<100:
  497. skillLevelStr='0'+skillLevelStr
  498. if skillLevel<10:
  499. skillLevelStr='0'+skillLevelStr
  500. indexStr=skillLevelStr+';'+actor+';'+actorJson['name']+';'+actorJson['icon']['url']
  501. if indexStr not in results:
  502. results.append(indexStr)
  503. results.sort(reverse=True)
  504. cssFilename=baseDir+'/epicyon-profile.css'
  505. if os.path.isfile(baseDir+'/epicyon.css'):
  506. cssFilename=baseDir+'/epicyon.css'
  507. with open(cssFilename, 'r') as cssFile:
  508. skillSearchCSS = cssFile.read()
  509. if httpPrefix!='https':
  510. skillSearchCSS=skillSearchCSS.replace('https://',httpPrefix+'://')
  511. skillSearchForm=htmlHeader(cssFilename,skillSearchCSS)
  512. skillSearchForm+='<center><h1>'+translate['Skills search']+': '+skillsearch+'</h1></center>'
  513. if len(results)==0:
  514. skillSearchForm+='<center><h5>'+translate['No results']+'</h5></center>'
  515. else:
  516. skillSearchForm+='<center>'
  517. ctr=0
  518. for skillMatch in results:
  519. skillMatchFields=skillMatch.split(';')
  520. if len(skillMatchFields)==4:
  521. actor=skillMatchFields[1]
  522. actorName=skillMatchFields[2]
  523. avatarUrl=skillMatchFields[3]
  524. skillSearchForm+='<div class="search-result""><a href="'+actor+'/skills">'
  525. skillSearchForm+='<img loading="lazy" src="'+avatarUrl+'"/><span class="search-result-text">'+actorName+'</span></a></div>'
  526. ctr+=1
  527. if ctr>=postsPerPage:
  528. break
  529. skillSearchForm+='</center>'
  530. skillSearchForm+=htmlFooter()
  531. return skillSearchForm
  532. def scheduledPostsExist(baseDir: str,nickname: str,domain: str) -> bool:
  533. """Returns true if there are posts scheduled to be delivered
  534. """
  535. scheduleIndexFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/schedule.index'
  536. if not os.path.isfile(scheduleIndexFilename):
  537. return False
  538. if '#users#' in open(scheduleIndexFilename).read():
  539. return True
  540. return False
  541. def htmlEditProfile(translate: {},baseDir: str,path: str,domain: str,port: int,httpPrefix: str) -> str:
  542. """Shows the edit profile screen
  543. """
  544. imageFormats='.png, .jpg, .jpeg, .gif, .webp'
  545. pathOriginal=path
  546. path=path.replace('/inbox','').replace('/outbox','').replace('/shares','')
  547. nickname=getNicknameFromActor(path)
  548. if not nickname:
  549. return ''
  550. domainFull=domain
  551. if port:
  552. if port!=80 and port!=443:
  553. if ':' not in domain:
  554. domainFull=domain+':'+str(port)
  555. actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
  556. if not os.path.isfile(actorFilename):
  557. return ''
  558. isBot=''
  559. isGroup=''
  560. followDMs=''
  561. removeTwitter=''
  562. mediaInstanceStr=''
  563. displayNickname=nickname
  564. bioStr=''
  565. donateUrl=''
  566. emailAddress=''
  567. PGPpubKey=''
  568. xmppAddress=''
  569. matrixAddress=''
  570. manuallyApprovesFollowers=''
  571. actorJson=loadJson(actorFilename)
  572. if actorJson:
  573. donateUrl=getDonationUrl(actorJson)
  574. xmppAddress=getXmppAddress(actorJson)
  575. matrixAddress=getMatrixAddress(actorJson)
  576. emailAddress=getEmailAddress(actorJson)
  577. PGPpubKey=getPGPpubKey(actorJson)
  578. if actorJson.get('name'):
  579. displayNickname=actorJson['name']
  580. if actorJson.get('summary'):
  581. bioStr=actorJson['summary'].replace('<p>','').replace('</p>','')
  582. if actorJson.get('manuallyApprovesFollowers'):
  583. if actorJson['manuallyApprovesFollowers']:
  584. manuallyApprovesFollowers='checked'
  585. else:
  586. manuallyApprovesFollowers=''
  587. if actorJson.get('type'):
  588. if actorJson['type']=='Service':
  589. isBot='checked'
  590. isGroup=''
  591. elif actorJson['type']=='Group':
  592. isGroup='checked'
  593. isBot=''
  594. if os.path.isfile(baseDir+'/accounts/'+nickname+'@'+domain+'/.followDMs'):
  595. followDMs='checked'
  596. if os.path.isfile(baseDir+'/accounts/'+nickname+'@'+domain+'/.removeTwitter'):
  597. removeTwitter='checked'
  598. mediaInstance=getConfigParam(baseDir,"mediaInstance")
  599. if mediaInstance:
  600. if mediaInstance==True:
  601. mediaInstanceStr='checked'
  602. filterStr=''
  603. filterFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/filters.txt'
  604. if os.path.isfile(filterFilename):
  605. with open(filterFilename, 'r') as filterfile:
  606. filterStr=filterfile.read()
  607. blockedStr=''
  608. blockedFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/blocking.txt'
  609. if os.path.isfile(blockedFilename):
  610. with open(blockedFilename, 'r') as blockedfile:
  611. blockedStr=blockedfile.read()
  612. allowedInstancesStr=''
  613. allowedInstancesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/allowedinstances.txt'
  614. if os.path.isfile(allowedInstancesFilename):
  615. with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
  616. allowedInstancesStr=allowedInstancesFile.read()
  617. skills=getSkills(baseDir,nickname,domain)
  618. skillsStr=''
  619. skillCtr=1
  620. if skills:
  621. for skillDesc,skillValue in skills.items():
  622. skillsStr+='<p><input type="text" placeholder="'+translate['Skill']+' '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="'+skillDesc+'" style="width:40%">'
  623. skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="'+str(skillValue)+'"></p>'
  624. skillCtr+=1
  625. skillsStr+='<p><input type="text" placeholder="Skill '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="" style="width:40%">'
  626. skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="50"></p>' \
  627. cssFilename=baseDir+'/epicyon-profile.css'
  628. if os.path.isfile(baseDir+'/epicyon.css'):
  629. cssFilename=baseDir+'/epicyon.css'
  630. with open(cssFilename, 'r') as cssFile:
  631. editProfileCSS = cssFile.read()
  632. if httpPrefix!='https':
  633. editProfileCSS=editProfileCSS.replace('https://',httpPrefix+'://')
  634. instanceStr=''
  635. moderatorsStr=''
  636. themesDropdown=''
  637. adminNickname=getConfigParam(baseDir,'admin')
  638. if path.startswith('/users/'+adminNickname+'/'):
  639. instanceDescription=getConfigParam(baseDir,'instanceDescription')
  640. instanceDescriptionShort=getConfigParam(baseDir,'instanceDescriptionShort')
  641. instanceTitle=getConfigParam(baseDir,'instanceTitle')
  642. instanceStr='<div class="container">'
  643. instanceStr+=' <label class="labels">'+translate['Instance Title']+'</label>'
  644. instanceStr+=' <input type="text" name="instanceTitle" value="'+instanceTitle+'"><br>'
  645. instanceStr+=' <label class="labels">'+translate['Instance Short Description']+'</label>'
  646. instanceStr+=' <input type="text" name="instanceDescriptionShort" value="'+instanceDescriptionShort+'"><br>'
  647. instanceStr+=' <label class="labels">'+translate['Instance Description']+'</label>'
  648. instanceStr+=' <textarea id="message" name="instanceDescription" style="height:200px">'+instanceDescription+'</textarea>'
  649. instanceStr+=' <label class="labels">'+translate['Instance Logo']+'</label>'
  650. instanceStr+=' <input type="file" id="instanceLogo" name="instanceLogo"'
  651. instanceStr+=' accept="'+imageFormats+'">'
  652. instanceStr+='</div>'
  653. moderators=''
  654. moderatorsFile=baseDir+'/accounts/moderators.txt'
  655. if os.path.isfile(moderatorsFile):
  656. with open(moderatorsFile, "r") as f:
  657. moderators = f.read()
  658. moderatorsStr='<div class="container">'
  659. moderatorsStr+=' <b>'+translate['Moderators']+'</b><br>'
  660. moderatorsStr+=' '+translate['A list of moderator nicknames. One per line.']
  661. moderatorsStr+=' <textarea id="message" name="moderators" placeholder="'+translate['List of moderator nicknames']+'..." style="height:200px">'+moderators+'</textarea>'
  662. moderatorsStr+='</div>'
  663. themesDropdown= '<div class="container">'
  664. themesDropdown+=' <b>'+translate['Theme']+'</b><br>'
  665. themesDropdown+=' <select id="themeDropdown" name="themeDropdown" class="theme">'
  666. themesDropdown+=' <option value="default">'+translate['Default']+'</option>'
  667. themesDropdown+=' <option value="light">'+translate['Light']+'</option>'
  668. themesDropdown+=' <option value="purple">'+translate['Purple']+'</option>'
  669. themesDropdown+=' <option value="hacker">'+translate['Hacker']+'</option>'
  670. themesDropdown+=' <option value="highvis">'+translate['HighVis']+'</option>'
  671. themesDropdown+=' </select><br>'
  672. themesDropdown+='</div>'
  673. themeName=getConfigParam(baseDir,'theme')
  674. themesDropdown=themesDropdown.replace('<option value="'+themeName+'">','<option value="'+themeName+'" selected>')
  675. editProfileForm=htmlHeader(cssFilename,editProfileCSS)
  676. editProfileForm+='<form enctype="multipart/form-data" method="POST" accept-charset="UTF-8" action="'+path+'/profiledata">'
  677. editProfileForm+=' <div class="vertical-center">'
  678. editProfileForm+=' <p class="new-post-text">'+translate['Profile for']+' '+nickname+'@'+domainFull+'</p>'
  679. editProfileForm+=' <div class="container">'
  680. editProfileForm+=' <input type="submit" name="submitProfile" value="'+translate['Submit']+'">'
  681. editProfileForm+=' <a href="'+pathOriginal+'"><button class="cancelbtn">'+translate['Cancel']+'</button></a>'
  682. editProfileForm+=' </div>'
  683. if scheduledPostsExist(baseDir,nickname,domain):
  684. editProfileForm+=' <div class="container">'
  685. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="removeScheduledPosts">'+translate['Remove scheduled posts']+'<br>'
  686. editProfileForm+=' </div>'
  687. editProfileForm+=' <div class="container">'
  688. editProfileForm+=' <label class="labels">'+translate['Nickname']+'</label>'
  689. editProfileForm+=' <input type="text" name="displayNickname" value="'+displayNickname+'"><br>'
  690. editProfileForm+=' <label class="labels">'+translate['Your bio']+'</label>'
  691. editProfileForm+=' <textarea id="message" name="bio" style="height:200px">'+bioStr+'</textarea>'
  692. editProfileForm+='<label class="labels">'+translate['Donations link']+'</label><br>'
  693. editProfileForm+=' <input type="text" placeholder="https://..." name="donateUrl" value="'+donateUrl+'">'
  694. editProfileForm+='<label class="labels">'+translate['XMPP']+'</label><br>'
  695. editProfileForm+=' <input type="text" name="xmppAddress" value="'+xmppAddress+'">'
  696. editProfileForm+='<label class="labels">'+translate['Matrix']+'</label><br>'
  697. editProfileForm+=' <input type="text" name="matrixAddress" value="'+matrixAddress+'">'
  698. editProfileForm+='<label class="labels">'+translate['Email']+'</label><br>'
  699. editProfileForm+=' <input type="text" name="email" value="'+emailAddress+'">'
  700. editProfileForm+='<label class="labels">'+translate['PGP']+'</label><br>'
  701. editProfileForm+=' <textarea id="message" placeholder="-----BEGIN PGP PUBLIC KEY BLOCK-----" name="pgp" style="height:100px">'+PGPpubKey+'</textarea>'
  702. editProfileForm+=' </div>'
  703. editProfileForm+=' <div class="container">'
  704. editProfileForm+=' <label class="labels">'+translate['The files attached below should be no larger than 10MB in total uploaded at once.']+'</label><br><br>'
  705. editProfileForm+=' <label class="labels">'+translate['Avatar image']+'</label>'
  706. editProfileForm+=' <input type="file" id="avatar" name="avatar"'
  707. editProfileForm+=' accept="'+imageFormats+'">'
  708. editProfileForm+=' <br><label class="labels">'+translate['Background image']+'</label>'
  709. editProfileForm+=' <input type="file" id="image" name="image"'
  710. editProfileForm+=' accept="'+imageFormats+'">'
  711. editProfileForm+=' <br><label class="labels">'+translate['Timeline banner image']+'</label>'
  712. editProfileForm+=' <input type="file" id="banner" name="banner"'
  713. editProfileForm+=' accept="'+imageFormats+'">'
  714. editProfileForm+=' </div>'
  715. editProfileForm+=' <div class="container">'
  716. editProfileForm+='<label class="labels">'+translate['Change Password']+'</label><br>'
  717. editProfileForm+=' <input type="text" name="password" value=""><br>'
  718. editProfileForm+='<label class="labels">'+translate['Confirm Password']+'</label><br>'
  719. editProfileForm+=' <input type="text" name="passwordconfirm" value="">'
  720. editProfileForm+=' </div>'
  721. editProfileForm+=' <div class="container">'
  722. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="approveFollowers" '+manuallyApprovesFollowers+'>'+translate['Approve follower requests']+'<br>'
  723. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="isBot" '+isBot+'>'+translate['This is a bot account']+'<br>'
  724. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="isGroup" '+isGroup+'>'+translate['This is a group account']+'<br>'
  725. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="followDMs" '+followDMs+'>'+translate['Only people I follow can send me DMs']+'<br>'
  726. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="removeTwitter" '+removeTwitter+'>'+translate['Remove Twitter posts']+'<br>'
  727. if path.startswith('/users/'+adminNickname+'/'):
  728. editProfileForm+=' <input type="checkbox" class="profilecheckbox" name="mediaInstance" '+mediaInstanceStr+'>'+translate['This is a media instance']+'<br>'
  729. editProfileForm+=' <br><b><label class="labels">'+translate['Filtered words']+'</label></b>'
  730. editProfileForm+=' <br><label class="labels">'+translate['One per line']+'</label>'
  731. editProfileForm+=' <textarea id="message" name="filteredWords" style="height:200px">'+filterStr+'</textarea>'
  732. editProfileForm+=' <br><b><label class="labels">'+translate['Blocked accounts']+'</label></b>'
  733. editProfileForm+=' <br><label class="labels">'+translate['Blocked accounts, one per line, in the form nickname@domain or *@blockeddomain']+'</label>'
  734. editProfileForm+=' <textarea id="message" name="blocked" style="height:200px">'+blockedStr+'</textarea>'
  735. editProfileForm+=' <br><b><label class="labels">'+translate['Federation list']+'</label></b>'
  736. editProfileForm+=' <br><label class="labels">'+translate['Federate only with a defined set of instances. One domain name per line.']+'</label>'
  737. editProfileForm+=' <textarea id="message" name="allowedInstances" style="height:200px">'+allowedInstancesStr+'</textarea>'
  738. editProfileForm+=' </div>'
  739. editProfileForm+=' <div class="container">'
  740. editProfileForm+=' <b><label class="labels">'+translate['Skills']+'</label></b><br>'
  741. editProfileForm+=' <label class="labels">'+translate['If you want to participate within organizations then you can indicate some skills that you have and approximate proficiency levels. This helps organizers to construct teams with an appropriate combination of skills.']+'</label>'
  742. editProfileForm+=skillsStr+themesDropdown+moderatorsStr
  743. editProfileForm+=' </div>'+instanceStr
  744. editProfileForm+=' <div class="container">'
  745. editProfileForm+=' <b><label class="labels">'+translate['Danger Zone']+'</label></b><br>'
  746. editProfileForm+=' <input type="checkbox" class=dangercheckbox" name="deactivateThisAccount">'+translate['Deactivate this account']+'<br>'
  747. editProfileForm+=' </div>'
  748. editProfileForm+=' </div>'
  749. editProfileForm+='</form>'
  750. editProfileForm+=htmlFooter()
  751. return editProfileForm
  752. def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str,bool):
  753. """Receives login credentials via HTTPServer POST
  754. """
  755. if not loginParams.startswith('username='):
  756. return None,None,None
  757. # minimum time between login attempts
  758. currTime=int(time.time())
  759. if currTime<lastLoginTime+10:
  760. return None,None,None
  761. if '&' not in loginParams:
  762. return None,None,None
  763. loginArgs=loginParams.split('&')
  764. nickname=None
  765. password=None
  766. register=False
  767. for arg in loginArgs:
  768. if '=' in arg:
  769. if arg.split('=',1)[0]=='username':
  770. nickname=arg.split('=',1)[1]
  771. elif arg.split('=',1)[0]=='password':
  772. password=arg.split('=',1)[1]
  773. elif arg.split('=',1)[0]=='register':
  774. register=True
  775. return nickname,password,register
  776. def htmlLogin(translate: {},baseDir: str,autocomplete=True) -> str:
  777. """Shows the login screen
  778. """
  779. accounts=noOfAccounts(baseDir)
  780. loginImage='login.png'
  781. loginImageFilename=None
  782. if os.path.isfile(baseDir+'/accounts/'+loginImage):
  783. loginImageFilename=baseDir+'/accounts/'+loginImage
  784. if os.path.isfile(baseDir+'/accounts/login.jpg'):
  785. loginImage='login.jpg'
  786. loginImageFilename=baseDir+'/accounts/'+loginImage
  787. if os.path.isfile(baseDir+'/accounts/login.jpeg'):
  788. loginImage='login.jpeg'
  789. loginImageFilename=baseDir+'/accounts/'+loginImage
  790. if os.path.isfile(baseDir+'/accounts/login.gif'):
  791. loginImage='login.gif'
  792. loginImageFilename=baseDir+'/accounts/'+loginImage
  793. if os.path.isfile(baseDir+'/accounts/login.webp'):
  794. loginImage='login.webp'
  795. loginImageFilename=baseDir+'/accounts/'+loginImage
  796. if not loginImageFilename:
  797. loginImageFilename=baseDir+'/accounts/'+loginImage
  798. copyfile(baseDir+'/img/login.png',loginImageFilename)
  799. if os.path.isfile(baseDir+'/img/login-background.png'):
  800. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  801. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  802. if accounts>0:
  803. loginText='<p class="login-text">'+translate['Welcome. Please enter your login details below.']+'</p>'
  804. else:
  805. loginText='<p class="login-text">'+translate['Please enter some credentials']+'</p>'
  806. loginText+='<p class="login-text">'+translate['You will become the admin of this site.']+'</p>'
  807. if os.path.isfile(baseDir+'/accounts/login.txt'):
  808. # custom login message
  809. with open(baseDir+'/accounts/login.txt', 'r') as file:
  810. loginText = '<p class="login-text">'+file.read()+'</p>'
  811. cssFilename=baseDir+'/epicyon-login.css'
  812. if os.path.isfile(baseDir+'/login.css'):
  813. cssFilename=baseDir+'/login.css'
  814. with open(cssFilename, 'r') as cssFile:
  815. loginCSS = cssFile.read()
  816. # show the register button
  817. registerButtonStr=''
  818. if getConfigParam(baseDir,'registration')=='open':
  819. if int(getConfigParam(baseDir,'registrationsRemaining'))>0:
  820. if accounts>0:
  821. loginText='<p class="login-text">'+translate['Welcome. Please login or register a new account.']+'</p>'
  822. registerButtonStr='<button type="submit" name="register">Register</button>'
  823. TOSstr='<p class="login-text"><a href="/terms">'+translate['Terms of Service']+'</a></p>'
  824. TOSstr+='<p class="login-text"><a href="/about">'+translate['About this Instance']+'</a></p>'
  825. loginButtonStr=''
  826. if accounts>0:
  827. loginButtonStr='<button type="submit" name="submit">'+translate['Login']+'</button>'
  828. autocompleteStr=''
  829. if not autocomplete:
  830. autocompleteStr='autocomplete="off" value=""'
  831. loginForm=htmlHeader(cssFilename,loginCSS)
  832. loginForm+='<form method="POST" action="/login">'
  833. loginForm+=' <div class="imgcontainer">'
  834. loginForm+=' <img loading="lazy" src="'+loginImage+'" alt="login image" class="loginimage">'
  835. loginForm+=loginText+TOSstr
  836. loginForm+=' </div>'
  837. loginForm+=''
  838. loginForm+=' <div class="container">'
  839. loginForm+=' <label for="nickname"><b>'+translate['Nickname']+'</b></label>'
  840. loginForm+=' <input type="text" '+autocompleteStr+' placeholder="'+translate['Enter Nickname']+'" name="username" required autofocus>'
  841. loginForm+=''
  842. loginForm+=' <label for="password"><b>'+translate['Password']+'</b></label>'
  843. loginForm+=' <input type="password" '+autocompleteStr+' placeholder="'+translate['Enter Password']+'" name="password" required>'
  844. loginForm+=registerButtonStr+loginButtonStr
  845. loginForm+=' </div>'
  846. loginForm+='</form>'
  847. loginForm+='<a href="https://gitlab.com/bashrc2/epicyon"><img loading="lazy" class="license" title="'+translate['Get the source code']+'" alt="'+translate['Get the source code']+'" src="/icons/agpl.png" /></a>'
  848. loginForm+=htmlFooter()
  849. return loginForm
  850. def htmlTermsOfService(baseDir: str,httpPrefix: str,domainFull: str) -> str:
  851. """Show the terms of service screen
  852. """
  853. adminNickname = getConfigParam(baseDir,'admin')
  854. if not os.path.isfile(baseDir+'/accounts/tos.txt'):
  855. copyfile(baseDir+'/default_tos.txt',baseDir+'/accounts/tos.txt')
  856. if os.path.isfile(baseDir+'/img/login-background.png'):
  857. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  858. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  859. TOSText='Terms of Service go here.'
  860. if os.path.isfile(baseDir+'/accounts/tos.txt'):
  861. with open(baseDir+'/accounts/tos.txt', 'r') as file:
  862. TOSText = file.read()
  863. TOSForm=''
  864. cssFilename=baseDir+'/epicyon-profile.css'
  865. if os.path.isfile(baseDir+'/epicyon.css'):
  866. cssFilename=baseDir+'/epicyon.css'
  867. with open(cssFilename, 'r') as cssFile:
  868. termsCSS = cssFile.read()
  869. if httpPrefix!='https':
  870. termsCSS=termsCSS.replace('https://',httpPrefix+'://')
  871. TOSForm=htmlHeader(cssFilename,termsCSS)
  872. TOSForm+='<div class="container">'+TOSText+'</div>'
  873. if adminNickname:
  874. adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
  875. TOSForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
  876. TOSForm+=htmlFooter()
  877. return TOSForm
  878. def htmlAbout(baseDir: str,httpPrefix: str,domainFull: str) -> str:
  879. """Show the about screen
  880. """
  881. adminNickname = getConfigParam(baseDir,'admin')
  882. if not os.path.isfile(baseDir+'/accounts/about.txt'):
  883. copyfile(baseDir+'/default_about.txt',baseDir+'/accounts/about.txt')
  884. if os.path.isfile(baseDir+'/img/login-background.png'):
  885. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  886. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  887. aboutText='Information about this instance goes here.'
  888. if os.path.isfile(baseDir+'/accounts/about.txt'):
  889. with open(baseDir+'/accounts/about.txt', 'r') as file:
  890. aboutText = file.read()
  891. aboutForm=''
  892. cssFilename=baseDir+'/epicyon-profile.css'
  893. if os.path.isfile(baseDir+'/epicyon.css'):
  894. cssFilename=baseDir+'/epicyon.css'
  895. with open(cssFilename, 'r') as cssFile:
  896. termsCSS = cssFile.read()
  897. if httpPrefix!='http':
  898. termsCSS=termsCSS.replace('https://',httpPrefix+'://')
  899. aboutForm=htmlHeader(cssFilename,termsCSS)
  900. aboutForm+='<div class="container">'+aboutText+'</div>'
  901. if adminNickname:
  902. adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
  903. aboutForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
  904. aboutForm+=htmlFooter()
  905. return aboutForm
  906. def htmlHashtagBlocked(baseDir: str) -> str:
  907. """Show the screen for a blocked hashtag
  908. """
  909. blockedHashtagForm=''
  910. cssFilename=baseDir+'/epicyon-suspended.css'
  911. if os.path.isfile(baseDir+'/suspended.css'):
  912. cssFilename=baseDir+'/suspended.css'
  913. with open(cssFilename, 'r') as cssFile:
  914. blockedHashtagCSS=cssFile.read()
  915. blockedHashtagForm=htmlHeader(cssFilename,blockedHashtagCSS)
  916. blockedHashtagForm+='<div><center>'
  917. blockedHashtagForm+=' <p class="screentitle">Hashtag Blocked</p>'
  918. blockedHashtagForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
  919. blockedHashtagForm+='</center></div>'
  920. blockedHashtagForm+=htmlFooter()
  921. return blockedHashtagForm
  922. def htmlSuspended(baseDir: str) -> str:
  923. """Show the screen for suspended accounts
  924. """
  925. suspendedForm=''
  926. cssFilename=baseDir+'/epicyon-suspended.css'
  927. if os.path.isfile(baseDir+'/suspended.css'):
  928. cssFilename=baseDir+'/suspended.css'
  929. with open(cssFilename, 'r') as cssFile:
  930. suspendedCSS=cssFile.read()
  931. suspendedForm=htmlHeader(cssFilename,suspendedCSS)
  932. suspendedForm+='<div><center>'
  933. suspendedForm+=' <p class="screentitle">Account Suspended</p>'
  934. suspendedForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
  935. suspendedForm+='</center></div>'
  936. suspendedForm+=htmlFooter()
  937. return suspendedForm
  938. def htmlNewPost(mediaInstance: bool,translate: {}, \
  939. baseDir: str,httpPrefix: str, \
  940. path: str,inReplyTo: str, \
  941. mentions: [], \
  942. reportUrl: str,pageNumber: int, \
  943. nickname: str,domain: str) -> str:
  944. """New post screen
  945. """
  946. iconsDir=getIconsDir(baseDir)
  947. replyStr=''
  948. showPublicOnDropdown=True
  949. if not path.endswith('/newshare'):
  950. if not path.endswith('/newreport'):
  951. if not inReplyTo:
  952. newPostText='<p class="new-post-text">'+translate['Write your post text below.']+'</p>'
  953. else:
  954. newPostText='<p class="new-post-text">'+translate['Write your reply to']+' <a href="'+inReplyTo+'">'+translate['this post']+'</a></p>'
  955. replyStr='<input type="hidden" name="replyTo" value="'+inReplyTo+'">'
  956. # if replying to a non-public post then also make this post non-public
  957. if not isPublicPostFromUrl(baseDir,nickname,domain,inReplyTo):
  958. newPostPath=path
  959. if '?' in newPostPath:
  960. newPostPath=newPostPath.split('?')[0]
  961. if newPostPath.endswith('/newpost'):
  962. path=path.replace('/newpost','/newfollowers')
  963. elif newPostPath.endswith('/newunlisted'):
  964. path=path.replace('/newunlisted','/newfollowers')
  965. showPublicOnDropdown=False
  966. else:
  967. newPostText= \
  968. '<p class="new-post-text">'+translate['Write your report below.']+'</p>'
  969. # custom report header with any additional instructions
  970. if os.path.isfile(baseDir+'/accounts/report.txt'):
  971. with open(baseDir+'/accounts/report.txt', 'r') as file:
  972. customReportText=file.read()
  973. if '</p>' not in customReportText:
  974. customReportText='<p class="login-subtext">'+customReportText+'</p>'
  975. customReportText=customReportText.replace('<p>','<p class="login-subtext">')
  976. newPostText+=customReportText
  977. newPostText+='<p class="new-post-subtext">'+translate['This message only goes to moderators, even if it mentions other fediverse addresses.']+'</p><p class="new-post-subtext">'+translate['Also see']+' <a href="/terms">'+translate['Terms of Service']+'</a></p>'
  978. else:
  979. newPostText='<p class="new-post-text">'+translate['Enter the details for your shared item below.']+'</p>'
  980. if path.endswith('/newquestion'):
  981. newPostText='<p class="new-post-text">'+translate['Enter the choices for your question below.']+'</p>'
  982. if os.path.isfile(baseDir+'/accounts/newpost.txt'):
  983. with open(baseDir+'/accounts/newpost.txt', 'r') as file:
  984. newPostText = '<p class="new-post-text">'+file.read()+'</p>'
  985. cssFilename=baseDir+'/epicyon-profile.css'
  986. if os.path.isfile(baseDir+'/epicyon.css'):
  987. cssFilename=baseDir+'/epicyon.css'
  988. with open(cssFilename, 'r') as cssFile:
  989. newPostCSS = cssFile.read()
  990. if httpPrefix!='https':
  991. newPostCSS=newPostCSS.replace('https://',httpPrefix+'://')
  992. if '?' in path:
  993. path=path.split('?')[0]
  994. pathBase=path.replace('/newreport','').replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','')
  995. newPostImageSection =' <div class="container">'
  996. newPostImageSection+=' <label class="labels">'+translate['Image description']+'</label>'
  997. newPostImageSection+=' <input type="text" name="imageDescription">'
  998. newPostImageSection+=' <input type="file" id="attachpic" name="attachpic"'
  999. newPostImageSection+=' accept=".png, .jpg, .jpeg, .gif, .webp, .mp4, .webm, .ogv, .mp3, .ogg">'
  1000. newPostImageSection+=' </div>'
  1001. scopeIcon='scope_public.png'
  1002. scopeDescription=translate['Public']
  1003. placeholderSubject=translate['Subject or Content Warning (optional)']+'...'
  1004. placeholderMessage=translate['Write something']+'...'
  1005. extraFields=''
  1006. endpoint='newpost'
  1007. if path.endswith('/newunlisted'):
  1008. scopeIcon='scope_unlisted.png'
  1009. scopeDescription=translate['Unlisted']
  1010. endpoint='newunlisted'
  1011. if path.endswith('/newfollowers'):
  1012. scopeIcon='scope_followers.png'
  1013. scopeDescription=translate['Followers']
  1014. endpoint='newfollowers'
  1015. if path.endswith('/newdm'):
  1016. scopeIcon='scope_dm.png'
  1017. scopeDescription=translate['DM']
  1018. endpoint='newdm'
  1019. if path.endswith('/newreport'):
  1020. scopeIcon='scope_report.png'
  1021. scopeDescription=translate['Report']
  1022. endpoint='newreport'
  1023. if path.endswith('/newquestion'):
  1024. scopeIcon='scope_question.png'
  1025. scopeDescription=translate['Question']
  1026. placeholderMessage=translate['Enter your question']+'...'
  1027. endpoint='newquestion'
  1028. extraFields='<div class="container">'
  1029. extraFields+=' <label class="labels">'+translate['Possible answers']+':</label><br>'
  1030. for questionCtr in range(8):
  1031. extraFields+=' <input type="text" class="questionOption" placeholder="'+str(questionCtr+1)+'" name="questionOption'+str(questionCtr)+'"><br>'
  1032. extraFields+=' <label class="labels">'+translate['Duration of listing in days']+':</label> <input type="number" name="duration" min="1" max="365" step="1" value="14"><br>'
  1033. extraFields+='</div>'
  1034. if path.endswith('/newshare'):
  1035. scopeIcon='scope_share.png'
  1036. scopeDescription=translate['Shared Item']
  1037. placeholderSubject=translate['Name of the shared item']+'...'
  1038. placeholderMessage=translate['Description of the item being shared']+'...'
  1039. endpoint='newshare'
  1040. extraFields='<div class="container">'
  1041. extraFields+=' <label class="labels">'+translate['Type of shared item. eg. hat']+':</label>'
  1042. extraFields+=' <input type="text" class="itemType" name="itemType">'
  1043. extraFields+=' <br><label class="labels">'+translate['Category of shared item. eg. clothing']+':</label>'
  1044. extraFields+=' <input type="text" class="category" name="category">'
  1045. extraFields+=' <br><label class="labels">'+translate['Duration of listing in days']+':</label>'
  1046. extraFields+=' <input type="number" name="duration" min="1" max="365" step="1" value="14">'
  1047. extraFields+='</div>'
  1048. extraFields+='<div class="container">'
  1049. extraFields+='<label class="labels">'+translate['City or location of the shared item']+':</label>'
  1050. extraFields+='<input type="text" name="location">'
  1051. extraFields+='</div>'
  1052. dateAndLocation=''
  1053. if endpoint!='newshare' and endpoint!='newreport' and endpoint!='newquestion':
  1054. dateAndLocation='<div class="container">'
  1055. if not inReplyTo:
  1056. dateAndLocation+='<p><input type="checkbox" class="profilecheckbox" name="schedulePost"><label class="labels">'+translate['This is a scheduled post.']+'</label></p>'
  1057. dateAndLocation+='<p><img loading="lazy" alt="" title="" class="emojicalendar" src="/'+iconsDir+'/calendar.png"/>'
  1058. dateAndLocation+='<label class="labels">'+translate['Date']+': </label>'
  1059. dateAndLocation+='<input type="date" name="eventDate">'
  1060. dateAndLocation+='<label class="labelsright">'+translate['Time']+':'
  1061. dateAndLocation+='<input type="time" name="eventTime"></label></p>'
  1062. dateAndLocation+='</div>'
  1063. dateAndLocation+='<div class="container">'
  1064. dateAndLocation+='<br><label class="labels">'+translate['Location']+': </label>'
  1065. dateAndLocation+='<input type="text" name="location">'
  1066. dateAndLocation+='</div>'
  1067. newPostForm=htmlHeader(cssFilename,newPostCSS)
  1068. # only show the share option if this is not a reply
  1069. shareOptionOnDropdown=''
  1070. questionOptionOnDropdown=''
  1071. if not replyStr:
  1072. shareOptionOnDropdown='<a href="'+pathBase+'/newshare"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_share.png"/><b>'+translate['Shares']+'</b><br>'+translate['Describe a shared item']+'</a>'
  1073. questionOptionOnDropdown='<a href="'+pathBase+'/newquestion"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_question.png"/><b>'+translate['Question']+'</b><br>'+translate['Ask a question']+'</a>'
  1074. mentionsStr=''
  1075. for m in mentions:
  1076. mentionNickname=getNicknameFromActor(m)
  1077. if not mentionNickname:
  1078. continue
  1079. mentionDomain,mentionPort=getDomainFromActor(m)
  1080. if not mentionDomain:
  1081. continue
  1082. if mentionPort:
  1083. mentionsHandle='@'+mentionNickname+'@'+mentionDomain+':'+str(mentionPort)
  1084. else:
  1085. mentionsHandle='@'+mentionNickname+'@'+mentionDomain
  1086. if mentionsHandle not in mentionsStr:
  1087. mentionsStr+=mentionsHandle+' '
  1088. # build suffixes so that any replies or mentions are preserved when switching between scopes
  1089. dropdownNewPostSuffix='/newpost'
  1090. dropdownUnlistedSuffix='/newunlisted'
  1091. dropdownFollowersSuffix='/newfollowers'
  1092. dropdownDMSuffix='/newdm'
  1093. dropdownReportSuffix='/newreport'
  1094. if inReplyTo or mentions:
  1095. dropdownNewPostSuffix=''
  1096. dropdownUnlistedSuffix=''
  1097. dropdownFollowersSuffix=''
  1098. dropdownDMSuffix=''
  1099. dropdownReportSuffix=''
  1100. if inReplyTo:
  1101. dropdownNewPostSuffix+='?replyto='+inReplyTo
  1102. dropdownUnlistedSuffix+='?replyto='+inReplyTo
  1103. dropdownFollowersSuffix+='?replyfollowers='+inReplyTo
  1104. dropdownDMSuffix+='?replydm='+inReplyTo
  1105. for mentionedActor in mentions:
  1106. dropdownNewPostSuffix+='?mention='+mentionedActor
  1107. dropdownUnlistedSuffix+='?mention='+mentionedActor
  1108. dropdownFollowersSuffix+='?mention='+mentionedActor
  1109. dropdownDMSuffix+='?mention='+mentionedActor
  1110. dropdownReportSuffix+='?mention='+mentionedActor
  1111. dropDownContent=''
  1112. if not reportUrl:
  1113. dropDownContent+=' <div id="myDropdown" class="dropdown-content">'
  1114. if showPublicOnDropdown:
  1115. dropDownContent+=' <a href="'+pathBase+dropdownNewPostSuffix+'"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_public.png"/><b>'+translate['Public']+'</b><br>'+translate['Visible to anyone']+'</a>'
  1116. dropDownContent+=' <a href="'+pathBase+dropdownUnlistedSuffix+'"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_unlisted.png"/><b>'+translate['Unlisted']+'</b><br>'+translate['Not on public timeline']+'</a>'
  1117. dropDownContent+=' <a href="'+pathBase+dropdownFollowersSuffix+'"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_followers.png"/><b>'+translate['Followers']+'</b><br>'+translate['Only to followers']+'</a>'
  1118. dropDownContent+=' <a href="'+pathBase+dropdownDMSuffix+'"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_dm.png"/><b>'+translate['DM']+'</b><br>'+translate['Only to mentioned people']+'</a>'
  1119. dropDownContent+=' <a href="'+pathBase+dropdownReportSuffix+'"><img loading="lazy" alt="" title="" src="/'+iconsDir+'/scope_report.png"/><b>'+translate['Report']+'</b><br>'+translate['Send to moderators']+'</a>'
  1120. dropDownContent+=questionOptionOnDropdown+shareOptionOnDropdown
  1121. dropDownContent+=' </div>'
  1122. else:
  1123. mentionsStr='Re: '+reportUrl+'\n\n'+mentionsStr
  1124. newPostForm+='<form enctype="multipart/form-data" method="POST" accept-charset="UTF-8" action="'+path+'?'+endpoint+'?page='+str(pageNumber)+'">'
  1125. newPostForm+=' <div class="vertical-center">'
  1126. newPostForm+=' <label for="nickname"><b>'+newPostText+'</b></label>'
  1127. newPostForm+=' <div class="container">'
  1128. newPostForm+=' <div class="dropbtn" onclick="dropdown()">'
  1129. newPostForm+=' <img loading="lazy" alt="" title="" src="/'+iconsDir+'/'+scopeIcon+'"/><b class="scope-desc">'+scopeDescription+'</b>'
  1130. newPostForm+=dropDownContent
  1131. newPostForm+=' </div>'
  1132. newPostForm+=' <a href="'+pathBase+'/searchemoji"><img loading="lazy" class="emojisearch" src="/emoji/1F601.png" title="'+translate['Search for emoji']+'" alt="'+translate['Search for emoji']+'"/></a>'
  1133. newPostForm+=' </div>'
  1134. newPostForm+=' <div class="container"><center>'
  1135. newPostForm+=' <a href="'+pathBase+'/inbox"><button class="cancelbtn">'+translate['Cancel']+'</button></a>'
  1136. newPostForm+=' <input type="submit" name="submitPost" value="'+translate['Submit']+'">'
  1137. newPostForm+=' </center></div>'
  1138. newPostForm+=replyStr
  1139. if mediaInstance and not replyStr:
  1140. newPostForm+=newPostImageSection
  1141. newPostForm+=' <label class="labels">'+placeholderSubject+'</label><br>'
  1142. newPostForm+=' <input type="text" name="subject">'
  1143. newPostForm+=''
  1144. newPostForm+=' <br><label class="labels">'+placeholderMessage+'</label>'
  1145. messageBoxHeight=400
  1146. if mediaInstance:
  1147. messageBoxHeight=200
  1148. if endpoint=='newquestion':
  1149. messageBoxHeight=100
  1150. newPostForm+=' <textarea id="message" name="message" style="height:'+str(messageBoxHeight)+'px">'+mentionsStr+'</textarea>'
  1151. newPostForm+=extraFields+dateAndLocation
  1152. if not mediaInstance or replyStr:
  1153. newPostForm+=newPostImageSection
  1154. newPostForm+=' </div>'
  1155. newPostForm+='</form>'
  1156. if not reportUrl:
  1157. newPostForm+='<script>'+clickToDropDownScript()+cursorToEndOfMessageScript()+'</script>'
  1158. newPostForm=newPostForm.replace('<body>','<body onload="focusOnMessage()">')
  1159. newPostForm+=htmlFooter()
  1160. return newPostForm
  1161. def htmlHeader(cssFilename: str,css=None,refreshSec=0,lang='en') -> str:
  1162. if refreshSec==0:
  1163. meta=' <meta charset="utf-8">\n'
  1164. else:
  1165. meta=' <meta http-equiv="Refresh" content="'+str(refreshSec)+'" charset="utf-8">\n'
  1166. if not css:
  1167. if '/' in cssFilename:
  1168. cssFilename=cssFilename.split('/')[-1]
  1169. htmlStr='<!DOCTYPE html>\n'
  1170. htmlStr+='<html lang="'+lang+'">\n'
  1171. htmlStr+=meta
  1172. htmlStr+=' <style>\n'
  1173. htmlStr+=' @import url("'+cssFilename+'");\n'
  1174. htmlStr+=' background-color: #282c37'
  1175. htmlStr+=' </style>\n'
  1176. htmlStr+=' <body>\n'
  1177. else:
  1178. htmlStr='<!DOCTYPE html>\n'
  1179. htmlStr+='<html lang="'+lang+'">\n'
  1180. htmlStr+=meta
  1181. htmlStr+=' <style>\n'+css+'</style>\n'
  1182. htmlStr+=' <body>\n'
  1183. return htmlStr
  1184. def htmlFooter() -> str:
  1185. htmlStr=' </body>\n'
  1186. htmlStr+='</html>\n'
  1187. return htmlStr
  1188. def htmlProfilePosts(recentPostsCache: {},maxRecentPosts: int, \
  1189. translate: {}, \
  1190. baseDir: str,httpPrefix: str, \
  1191. authorized: bool,ocapAlways: bool, \
  1192. nickname: str,domain: str,port: int, \
  1193. session,wfRequest: {},personCache: {}, \
  1194. projectVersion: str) -> str:
  1195. """Shows posts on the profile screen
  1196. These should only be public posts
  1197. """
  1198. iconsDir=getIconsDir(baseDir)
  1199. profileStr=''
  1200. maxItems=4
  1201. profileStr+='<script>'+contentWarningScript()+'</script>'
  1202. ctr=0
  1203. currPage=1
  1204. while ctr<maxItems and currPage<4:
  1205. outboxFeed= \
  1206. personBoxJson({},session,baseDir,domain, \
  1207. port,'/users/'+nickname+'/outbox?page='+str(currPage), \
  1208. httpPrefix, \
  1209. 10, 'outbox', \
  1210. authorized, \
  1211. ocapAlways)
  1212. if not outboxFeed:
  1213. break
  1214. if len(outboxFeed['orderedItems'])==0:
  1215. break
  1216. for item in outboxFeed['orderedItems']:
  1217. if item['type']=='Create':
  1218. postStr= \
  1219. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  1220. iconsDir,translate,None, \
  1221. baseDir,session,wfRequest,personCache, \
  1222. nickname,domain,port,item,None,True,False, \
  1223. httpPrefix,projectVersion,'inbox', \
  1224. False,False,False,True,False)
  1225. if postStr:
  1226. profileStr+=postStr
  1227. ctr+=1
  1228. if ctr>=maxItems:
  1229. break
  1230. currPage+=1
  1231. return profileStr
  1232. def htmlProfileFollowing(translate: {},baseDir: str,httpPrefix: str, \
  1233. authorized: bool,ocapAlways: bool, \
  1234. nickname: str,domain: str,port: int, \
  1235. session,wfRequest: {},personCache: {}, \
  1236. followingJson: {},projectVersion: str, \
  1237. buttons: [], \
  1238. feedName: str,actor: str, \
  1239. pageNumber: int, \
  1240. maxItemsPerPage: int) -> str:
  1241. """Shows following on the profile screen
  1242. """
  1243. profileStr=''
  1244. iconsDir=getIconsDir(baseDir)
  1245. if authorized and pageNumber:
  1246. if authorized and pageNumber>1:
  1247. # page up arrow
  1248. profileStr+= \
  1249. '<center><a href="'+actor+'/'+feedName+'?page='+str(pageNumber-1)+'"><img loading="lazy" class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"></a></center>'
  1250. for item in followingJson['orderedItems']:
  1251. profileStr+= \
  1252. individualFollowAsHtml(translate,baseDir,session, \
  1253. wfRequest,personCache, \
  1254. domain,item,authorized,nickname, \
  1255. httpPrefix,projectVersion, \
  1256. buttons)
  1257. if authorized and maxItemsPerPage and pageNumber:
  1258. if len(followingJson['orderedItems'])>=maxItemsPerPage:
  1259. # page down arrow
  1260. profileStr+= \
  1261. '<center><a href="'+actor+'/'+feedName+'?page='+str(pageNumber+1)+'"><img loading="lazy" class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"></a></center>'
  1262. return profileStr
  1263. def htmlProfileRoles(translate: {},nickname: str,domain: str,rolesJson: {}) -> str:
  1264. """Shows roles on the profile screen
  1265. """
  1266. profileStr=''
  1267. for project,rolesList in rolesJson.items():
  1268. profileStr+='<div class="roles"><h2>'+project+'</h2><div class="roles-inner">'
  1269. for role in rolesList:
  1270. profileStr+='<h3>'+role+'</h3>'
  1271. profileStr+='</div></div>'
  1272. if len(profileStr)==0:
  1273. profileStr+='<p>@'+nickname+'@'+domain+' has no roles assigned</p>'
  1274. else:
  1275. profileStr='<div>'+profileStr+'</div>'
  1276. return profileStr
  1277. def htmlProfileSkills(translate: {},nickname: str,domain: str,skillsJson: {}) -> str:
  1278. """Shows skills on the profile screen
  1279. """
  1280. profileStr=''
  1281. for skill,level in skillsJson.items():
  1282. profileStr+='<div>'+skill+'<br><div id="myProgress"><div id="myBar" style="width:'+str(level)+'%"></div></div></div><br>'
  1283. if len(profileStr)>0:
  1284. profileStr='<center><div class="skill-title">'+profileStr+'</div></center>'
  1285. return profileStr
  1286. def htmlIndividualShare(actor: str,item: {},translate: {},showContact: bool,removeButton: bool) -> str:
  1287. """Returns an individual shared item as html
  1288. """
  1289. profileStr='<div class="container">'
  1290. profileStr+='<p class="share-title">'+item['displayName']+'</p>'
  1291. if item.get('imageUrl'):
  1292. profileStr+='<a href="'+item['imageUrl']+'">'
  1293. profileStr+='<img loading="lazy" src="'+item['imageUrl']+'" alt="'+translate['Item image']+'"></a>'
  1294. profileStr+='<p>'+item['summary']+'</p>'
  1295. profileStr+='<p><b>'+translate['Type']+':</b> '+item['itemType']+' '
  1296. profileStr+='<b>'+translate['Category']+':</b> '+item['category']+' '
  1297. profileStr+='<b>'+translate['Location']+':</b> '+item['location']+'</p>'
  1298. if showContact:
  1299. contactActor=item['actor']
  1300. profileStr+='<p><a href="'+actor+'?replydm=sharedesc:'+item['displayName']+'?mention='+contactActor+'"><button class="button">'+translate['Contact']+'</button></a>'
  1301. if removeButton:
  1302. profileStr+=' <a href="'+actor+'?rmshare='+item['displayName']+'"><button class="button">'+translate['Remove']+'</button></a>'
  1303. profileStr+='</div>'
  1304. return profileStr
  1305. def htmlProfileShares(actor: str,translate: {},nickname: str,domain: str,sharesJson: {}) -> str:
  1306. """Shows shares on the profile screen
  1307. """
  1308. profileStr=''
  1309. for item in sharesJson['orderedItems']:
  1310. profileStr+=htmlIndividualShare(actor,item,translate,False,False)
  1311. if len(profileStr)>0:
  1312. profileStr='<div class="share-title">'+profileStr+'</div>'
  1313. return profileStr
  1314. def sharesTimelineJson(actor: str,pageNumber: int,itemsPerPage: int, \
  1315. baseDir: str,maxSharesPerAccount: int) -> ({},bool):
  1316. """Get a page on the shared items timeline as json
  1317. maxSharesPerAccount helps to avoid one person dominating the timeline
  1318. by sharing a large number of things
  1319. """
  1320. allSharesJson={}
  1321. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  1322. for handle in dirs:
  1323. if '@' in handle:
  1324. accountDir=baseDir+'/accounts/'+handle
  1325. sharesFilename=accountDir+'/shares.json'
  1326. if os.path.isfile(sharesFilename):
  1327. sharesJson=loadJson(sharesFilename)
  1328. if not sharesJson:
  1329. continue
  1330. nickname=handle.split('@')[0]
  1331. # actor who owns this share
  1332. owner=actor.split('/users/')[0]+'/users/'+nickname
  1333. ctr=0
  1334. for itemID,item in sharesJson.items():
  1335. # assign owner to the item
  1336. item['actor']=owner
  1337. allSharesJson[str(item['published'])]=item
  1338. ctr+=1
  1339. if ctr>=maxSharesPerAccount:
  1340. break
  1341. # sort the shared items in descending order of publication date
  1342. sharesJson=OrderedDict(sorted(allSharesJson.items(),reverse=True))
  1343. lastPage=False
  1344. startIndex=itemsPerPage*pageNumber
  1345. maxIndex=len(sharesJson.items())
  1346. if maxIndex<itemsPerPage:
  1347. lastPage=True
  1348. if startIndex>=maxIndex-itemsPerPage:
  1349. lastPage=True
  1350. startIndex=maxIndex-itemsPerPage
  1351. if startIndex<0:
  1352. startIndex=0
  1353. ctr=0
  1354. resultJson={}
  1355. for published,item in sharesJson.items():
  1356. if ctr>=startIndex+itemsPerPage:
  1357. break
  1358. if ctr<startIndex:
  1359. ctr+=1
  1360. continue
  1361. resultJson[published]=item
  1362. ctr+=1
  1363. return resultJson,lastPage
  1364. def htmlSharesTimeline(translate: {},pageNumber: int,itemsPerPage: int, \
  1365. baseDir: str,actor: str, \
  1366. nickname: str,domain: str,port: int, \
  1367. maxSharesPerAccount: int,httpPrefix: str) -> str:
  1368. """Show shared items timeline as html
  1369. """
  1370. sharesJson,lastPage= \
  1371. sharesTimelineJson(actor,pageNumber,itemsPerPage, \
  1372. baseDir,maxSharesPerAccount)
  1373. domainFull=domain
  1374. if port!=80 and port!=443:
  1375. if ':' not in domain:
  1376. domainFull=domain+':'+str(port)
  1377. actor=httpPrefix+'://'+domainFull+'/users/'+nickname
  1378. timelineStr=''
  1379. if pageNumber>1:
  1380. timelineStr+='<center><a href="'+actor+'/tlshares?page='+str(pageNumber-1)+'"><img loading="lazy" class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"></a></center>'
  1381. for published,item in sharesJson.items():
  1382. showContactButton=False
  1383. if item['actor']!=actor:
  1384. showContactButton=True
  1385. showRemoveButton=False
  1386. if item['actor']==actor:
  1387. showRemoveButton=True
  1388. timelineStr+=htmlIndividualShare(actor,item,translate,showContactButton,showRemoveButton)
  1389. if not lastPage:
  1390. timelineStr+='<center><a href="'+actor+'/tlshares?page='+str(pageNumber+1)+'"><img loading="lazy" class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"></a></center>'
  1391. return timelineStr
  1392. def htmlProfile(defaultTimeline: str, \
  1393. recentPostsCache: {},maxRecentPosts: int, \
  1394. translate: {},projectVersion: str, \
  1395. baseDir: str,httpPrefix: str,authorized: bool, \
  1396. ocapAlways: bool,profileJson: {},selected: str, \
  1397. session,wfRequest: {},personCache: {}, \
  1398. extraJson=None, \
  1399. pageNumber=None,maxItemsPerPage=None) -> str:
  1400. """Show the profile page as html
  1401. """
  1402. nickname=profileJson['preferredUsername']
  1403. if not nickname:
  1404. return ""
  1405. domain,port=getDomainFromActor(profileJson['id'])
  1406. if not domain:
  1407. return ""
  1408. displayName= \
  1409. addEmojiToDisplayName(baseDir,httpPrefix, \
  1410. nickname,domain, \
  1411. profileJson['name'],True)
  1412. domainFull=domain
  1413. if port:
  1414. domainFull=domain+':'+str(port)
  1415. profileDescription= \
  1416. addEmojiToDisplayName(baseDir,httpPrefix, \
  1417. nickname,domain, \
  1418. profileJson['summary'],False)
  1419. postsButton='button'
  1420. followingButton='button'
  1421. followersButton='button'
  1422. rolesButton='button'
  1423. skillsButton='button'
  1424. sharesButton='button'
  1425. if selected=='posts':
  1426. postsButton='buttonselected'
  1427. elif selected=='following':
  1428. followingButton='buttonselected'
  1429. elif selected=='followers':
  1430. followersButton='buttonselected'
  1431. elif selected=='roles':
  1432. rolesButton='buttonselected'
  1433. elif selected=='skills':
  1434. skillsButton='buttonselected'
  1435. elif selected=='shares':
  1436. sharesButton='buttonselected'
  1437. loginButton=''
  1438. followApprovalsSection=''
  1439. followApprovals=False
  1440. linkToTimelineStart=''
  1441. linkToTimelineEnd=''
  1442. editProfileStr=''
  1443. logoutStr=''
  1444. actor=profileJson['id']
  1445. donateSection=''
  1446. donateUrl=getDonationUrl(profileJson)
  1447. PGPpubKey=getPGPpubKey(profileJson)
  1448. emailAddress=getEmailAddress(profileJson)
  1449. xmppAddress=getXmppAddress(profileJson)
  1450. matrixAddress=getMatrixAddress(profileJson)
  1451. if donateUrl or xmppAddress or matrixAddress or PGPpubKey or emailAddress:
  1452. donateSection='<div class="container">\n'
  1453. donateSection+=' <center>\n'
  1454. if donateUrl:
  1455. donateSection+=' <p><a href="'+donateUrl+'"><button class="donateButton">'+translate['Donate']+'</button></a></p>\n'
  1456. if emailAddress:
  1457. donateSection+='<p>'+translate['Email']+': <a href="mailto:'+emailAddress+'">'+emailAddress+'</a></p>\n'
  1458. if xmppAddress:
  1459. donateSection+='<p>'+translate['XMPP']+': <a href="xmpp:'+xmppAddress+'">'+xmppAddress+'</a></p>\n'
  1460. if matrixAddress:
  1461. donateSection+='<p>'+translate['Matrix']+': '+matrixAddress+'</p>\n'
  1462. if PGPpubKey:
  1463. donateSection+='<p class="pgp">'+PGPpubKey.replace('\n','<br>')+'</p>\n'
  1464. donateSection+=' </center>\n'
  1465. donateSection+='</div>\n'
  1466. if not authorized:
  1467. loginButton='<br><a href="/login"><button class="loginButton">'+translate['Login']+'</button></a>'
  1468. else:
  1469. editProfileStr='<a href="'+actor+'/editprofile"><button class="button"><span>'+translate['Edit']+' </span></button></a>'
  1470. logoutStr='<a href="/logout"><button class="button"><span>'+translate['Logout']+' </span></button></a>'
  1471. linkToTimelineStart='<a href="/users/'+nickname+'/'+defaultTimeline+'"><label class="transparent">'+translate['Switch to timeline view']+'</label></a>'
  1472. linkToTimelineStart+='<a href="/users/'+nickname+'/'+defaultTimeline+'" title="'+translate['Switch to timeline view']+'" alt="'+translate['Switch to timeline view']+'">'
  1473. linkToTimelineEnd='</a>'
  1474. # are there any follow requests?
  1475. followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
  1476. if os.path.isfile(followRequestsFilename):
  1477. with open(followRequestsFilename,'r') as f:
  1478. for line in f:
  1479. if len(line)>0:
  1480. followApprovals=True
  1481. followersButton='buttonhighlighted'
  1482. if selected=='followers':
  1483. followersButton='buttonselectedhighlighted'
  1484. break
  1485. if selected=='followers':
  1486. if followApprovals:
  1487. with open(followRequestsFilename,'r') as f:
  1488. for followerHandle in f:
  1489. if len(line)>0:
  1490. if '://' in followerHandle:
  1491. followerActor=followerHandle
  1492. else:
  1493. followerActor=httpPrefix+'://'+followerHandle.split('@')[1]+'/users/'+followerHandle.split('@')[0]
  1494. basePath=httpPrefix+'://'+domainFull+'/users/'+nickname
  1495. followApprovalsSection+='<div class="container">'
  1496. followApprovalsSection+='<a href="'+followerActor+'">'
  1497. followApprovalsSection+='<span class="followRequestHandle">'+followerHandle+'</span></a>'
  1498. followApprovalsSection+='<a href="'+basePath+'/followapprove='+followerHandle+'">'
  1499. followApprovalsSection+='<button class="followApprove">'+translate['Approve']+'</button></a>'
  1500. followApprovalsSection+='<a href="'+basePath+'/followdeny='+followerHandle+'">'
  1501. followApprovalsSection+='<button class="followDeny">'+translate['Deny']+'</button></a>'
  1502. followApprovalsSection+='</div>'
  1503. profileDescriptionShort=profileDescription
  1504. if '\n' in profileDescription:
  1505. if len(profileDescription.split('\n'))>2:
  1506. profileDescriptionShort=''
  1507. else:
  1508. if '<br>' in profileDescription:
  1509. if len(profileDescription.split('<br>'))>2:
  1510. profileDescriptionShort=''
  1511. profileDescription=profileDescription.replace('<br>','\n')
  1512. # keep the profile description short
  1513. if len(profileDescriptionShort)>256:
  1514. profileDescriptionShort=''
  1515. # remove formatting from profile description used on title
  1516. avatarDescription=''
  1517. if profileJson.get('summary'):
  1518. avatarDescription=profileJson['summary'].replace('<br>','\n').replace('<p>','').replace('</p>','')
  1519. profileHeaderStr='<div class="hero-image">'
  1520. profileHeaderStr+=' <div class="hero-text">'
  1521. profileHeaderStr+=' <img loading="lazy" src="'+profileJson['icon']['url']+'" title="'+avatarDescription+'" alt="'+avatarDescription+'" class="title">'
  1522. profileHeaderStr+=' <h1>'+displayName+'</h1>'
  1523. profileHeaderStr+=' <p><b>@'+nickname+'@'+domainFull+'</b></p>'
  1524. profileHeaderStr+=' <p>'+profileDescriptionShort+'</p>'
  1525. profileHeaderStr+=loginButton
  1526. profileHeaderStr+=' </div>'
  1527. profileHeaderStr+='</div>'
  1528. profileStr=linkToTimelineStart + profileHeaderStr + linkToTimelineEnd + donateSection
  1529. profileStr+='<div class="container">\n'
  1530. profileStr+=' <center>'
  1531. profileStr+=' <a href="'+actor+'"><button class="'+postsButton+'"><span>'+translate['Posts']+' </span></button></a>'
  1532. profileStr+=' <a href="'+actor+'/following"><button class="'+followingButton+'"><span>'+translate['Following']+' </span></button></a>'
  1533. profileStr+=' <a href="'+actor+'/followers"><button class="'+followersButton+'"><span>'+translate['Followers']+' </span></button></a>'
  1534. profileStr+=' <a href="'+actor+'/roles"><button class="'+rolesButton+'"><span>'+translate['Roles']+' </span></button></a>'
  1535. profileStr+=' <a href="'+actor+'/skills"><button class="'+skillsButton+'"><span>'+translate['Skills']+' </span></button></a>'
  1536. profileStr+=' <a href="'+actor+'/shares"><button class="'+sharesButton+'"><span>'+translate['Shares']+' </span></button></a>'
  1537. profileStr+=editProfileStr+logoutStr
  1538. profileStr+=' </center>'
  1539. profileStr+='</div>'
  1540. profileStr+=followApprovalsSection
  1541. cssFilename=baseDir+'/epicyon-profile.css'
  1542. if os.path.isfile(baseDir+'/epicyon.css'):
  1543. cssFilename=baseDir+'/epicyon.css'
  1544. with open(cssFilename, 'r') as cssFile:
  1545. profileStyle = cssFile.read().replace('image.png',profileJson['image']['url'])
  1546. licenseStr='<a href="https://gitlab.com/bashrc2/epicyon"><img loading="lazy" class="license" alt="'+translate['Get the source code']+'" title="'+translate['Get the source code']+'" src="/icons/agpl.png" /></a>'
  1547. if selected=='posts':
  1548. profileStr+= \
  1549. htmlProfilePosts(recentPostsCache,maxRecentPosts, \
  1550. translate, \
  1551. baseDir,httpPrefix,authorized, \
  1552. ocapAlways,nickname,domain,port, \
  1553. session,wfRequest,personCache, \
  1554. projectVersion)+licenseStr
  1555. if selected=='following':
  1556. profileStr+= \
  1557. htmlProfileFollowing(translate,baseDir,httpPrefix, \
  1558. authorized,ocapAlways,nickname, \
  1559. domain,port,session, \
  1560. wfRequest,personCache,extraJson, \
  1561. projectVersion, \
  1562. ["unfollow"], \
  1563. selected,actor, \
  1564. pageNumber,maxItemsPerPage)
  1565. if selected=='followers':
  1566. profileStr+= \
  1567. htmlProfileFollowing(translate,baseDir,httpPrefix, \
  1568. authorized,ocapAlways,nickname, \
  1569. domain,port,session, \
  1570. wfRequest,personCache,extraJson, \
  1571. projectVersion, \
  1572. ["block"], \
  1573. selected,actor, \
  1574. pageNumber,maxItemsPerPage)
  1575. if selected=='roles':
  1576. profileStr+= \
  1577. htmlProfileRoles(translate,nickname,domainFull,extraJson)
  1578. if selected=='skills':
  1579. profileStr+= \
  1580. htmlProfileSkills(translate,nickname,domainFull,extraJson)
  1581. if selected=='shares':
  1582. profileStr+= \
  1583. htmlProfileShares(actor,translate,nickname,domainFull,extraJson)+licenseStr
  1584. profileStr=htmlHeader(cssFilename,profileStyle)+profileStr+htmlFooter()
  1585. return profileStr
  1586. def individualFollowAsHtml(translate: {}, \
  1587. baseDir: str,session,wfRequest: {}, \
  1588. personCache: {},domain: str, \
  1589. followUrl: str, \
  1590. authorized: bool, \
  1591. actorNickname: str, \
  1592. httpPrefix: str, \
  1593. projectVersion: str, \
  1594. buttons=[]) -> str:
  1595. nickname=getNicknameFromActor(followUrl)
  1596. domain,port=getDomainFromActor(followUrl)
  1597. titleStr='@'+nickname+'@'+domain
  1598. avatarUrl=getPersonAvatarUrl(baseDir,followUrl,personCache)
  1599. if not avatarUrl:
  1600. avatarUrl=followUrl+'/avatar.png'
  1601. if domain not in followUrl:
  1602. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,displayName = \
  1603. getPersonBox(baseDir,session,wfRequest,personCache, \
  1604. projectVersion,httpPrefix,nickname,domain,'outbox')
  1605. if avatarUrl2:
  1606. avatarUrl=avatarUrl2
  1607. if displayName:
  1608. titleStr=displayName+' '+titleStr
  1609. buttonsStr=''
  1610. if authorized:
  1611. for b in buttons:
  1612. if b=='block':
  1613. buttonsStr+='<a href="/users/'+actorNickname+'?options='+followUrl+';1;'+avatarUrl+'"><button class="buttonunfollow">'+translate['Block']+'</button></a>'
  1614. #buttonsStr+='<a href="/users/'+actorNickname+'?block='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">'+translate['Block']+'</button></a>'
  1615. if b=='unfollow':
  1616. buttonsStr+='<a href="/users/'+actorNickname+'?options='+followUrl+';1;'+avatarUrl+'"><button class="buttonunfollow">'+translate['Unfollow']+'</button></a>'
  1617. #buttonsStr+='<a href="/users/'+actorNickname+'?unfollow='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">'+translate['Unfollow']+'</button></a>'
  1618. resultStr='<div class="container">\n'
  1619. resultStr+='<a href="'+followUrl+'">'
  1620. resultStr+='<p><img loading="lazy" src="'+avatarUrl+'" alt=" ">\n'
  1621. resultStr+=titleStr+'</a>'+buttonsStr+'</p>'
  1622. resultStr+='</div>\n'
  1623. return resultStr
  1624. def clickToDropDownScript() -> str:
  1625. """Function run onclick to create a dropdown
  1626. """
  1627. script='function dropdown() {\n'
  1628. script+=' document.getElementById("myDropdown").classList.toggle("show");\n'
  1629. script+='}\n'
  1630. return script
  1631. def cursorToEndOfMessageScript() -> str:
  1632. """Moves the cursor to the end of the text in a textarea
  1633. This avoids the cursor being in the wrong position when replying
  1634. """
  1635. script='function focusOnMessage() {\n'
  1636. script+=" var replyTextArea = document.getElementById('message');\n"
  1637. script+=' val = replyTextArea.value;\n'
  1638. script+=' if ((val.length>0) && (val.charAt(val.length-1) != " ")) {\n'
  1639. script+=' val += " ";\n'
  1640. script+=' }\n'
  1641. script+=' replyTextArea.focus();\n'
  1642. script+=' replyTextArea.value="";\n'
  1643. script+=' replyTextArea.value=val;\n'
  1644. script+='}\n'
  1645. script+="var replyTextArea = document.getElementById('message')\n"
  1646. script+='replyTextArea.onFocus = function() {\n'
  1647. script+=' focusOnMessage();'
  1648. script+='}\n'
  1649. return script
  1650. def contentWarningScript() -> str:
  1651. """Returns a script used for content warnings
  1652. """
  1653. script='function showContentWarning(postID) {\n'
  1654. script+=' var x = document.getElementById(postID);\n'
  1655. script+=' if (x.style.display !== "block") {\n'
  1656. script+=' x.style.display = "block";\n'
  1657. script+=' } else {\n'
  1658. script+=' x.style.display = "none";\n'
  1659. script+=' }\n'
  1660. script+='}\n'
  1661. return script
  1662. def addEmbeddedAudio(translate: {},content: str) -> str:
  1663. """Adds embedded audio for mp3/ogg
  1664. """
  1665. if not ('.mp3' in content or '.ogg' in content):
  1666. return content
  1667. if '<audio ' in content:
  1668. return content
  1669. extension='.mp3'
  1670. if '.ogg' in content:
  1671. extension='.ogg'
  1672. words=content.strip('\n').split(' ')
  1673. for w in words:
  1674. if extension not in w:
  1675. continue
  1676. w=w.replace('href="','').replace('">','')
  1677. if w.endswith('.'):
  1678. w=w[:-1]
  1679. if w.endswith('"'):
  1680. w=w[:-1]
  1681. if w.endswith(';'):
  1682. w=w[:-1]
  1683. if w.endswith(':'):
  1684. w=w[:-1]
  1685. if not w.endswith(extension):
  1686. continue
  1687. if not (w.startswith('http') or w.startswith('dat:') or w.startswith('i2p:') or '/' in w):
  1688. continue
  1689. url=w
  1690. content+='<center><audio controls>'
  1691. content+='<source src="'+url+'" type="audio/'+extension.replace('.','')+'">'
  1692. content+=translate['Your browser does not support the audio element.']
  1693. content+='</audio></center>'
  1694. return content
  1695. def addEmbeddedVideo(translate: {},content: str,width=400,height=300) -> str:
  1696. """Adds embedded video for mp4/webm/ogv
  1697. """
  1698. if not ('.mp4' in content or '.webm' in content or '.ogv' in content):
  1699. return content
  1700. if '<video ' in content:
  1701. return content
  1702. extension='.mp4'
  1703. if '.webm' in content:
  1704. extension='.webm'
  1705. elif '.ogv' in content:
  1706. extension='.ogv'
  1707. words=content.strip('\n').split(' ')
  1708. for w in words:
  1709. if extension not in w:
  1710. continue
  1711. w=w.replace('href="','').replace('">','')
  1712. if w.endswith('.'):
  1713. w=w[:-1]
  1714. if w.endswith('"'):
  1715. w=w[:-1]
  1716. if w.endswith(';'):
  1717. w=w[:-1]
  1718. if w.endswith(':'):
  1719. w=w[:-1]
  1720. if not w.endswith(extension):
  1721. continue
  1722. if not (w.startswith('http') or w.startswith('dat:') or w.startswith('i2p:') or '/' in w):
  1723. continue
  1724. url=w
  1725. content+='<center><video width="'+str(width)+'" height="'+str(height)+'" controls>'
  1726. content+='<source src="'+url+'" type="video/'+extension.replace('.','')+'">'
  1727. content+=translate['Your browser does not support the video element.']
  1728. content+='</video></center>'
  1729. return content
  1730. def addEmbeddedVideoFromSites(translate: {},content: str,width=400,height=300) -> str:
  1731. """Adds embedded videos
  1732. """
  1733. if '>vimeo.com/' in content:
  1734. url=content.split('>vimeo.com/')[1]
  1735. if '<' in url:
  1736. url=url.split('<')[0]
  1737. content=content+"<center><iframe loading=\"lazy\" src=\"https://player.vimeo.com/video/"+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1738. return content
  1739. videoSite='https://www.youtube.com'
  1740. if '"'+videoSite in content:
  1741. url=content.split('"'+videoSite)[1]
  1742. if '"' in url:
  1743. url=url.split('"')[0].replace('/watch?v=','/embed/')
  1744. if '&' in url:
  1745. url=url.split('&')[0]
  1746. content=content+"<center><iframe loading=\"lazy\" src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1747. return content
  1748. invidiousSites=('https://invidio.us','axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid.onion')
  1749. for videoSite in invidiousSites:
  1750. if '"'+videoSite in content:
  1751. url=content.split('"'+videoSite)[1]
  1752. if '"' in url:
  1753. url=url.split('"')[0].replace('/watch?v=','/embed/')
  1754. if '&' in url:
  1755. url=url.split('&')[0]
  1756. content=content+"<center><iframe loading=\"lazy\" src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1757. return content
  1758. videoSite='https://media.ccc.de'
  1759. if '"'+videoSite in content:
  1760. url=content.split('"'+videoSite)[1]
  1761. if '"' in url:
  1762. url=url.split('"')[0]
  1763. if not url.endswith('/oembed'):
  1764. url=url+'/oembed'
  1765. content=content+"<center><iframe loading=\"lazy\" src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"fullscreen\" allowfullscreen></iframe></center>"
  1766. return content
  1767. if '"https://' in content:
  1768. # A selection of the current larger peertube sites, mostly French and German language
  1769. # These have been chosen based on reported numbers of users and the content of each has not been reviewed, so mileage could vary
  1770. peerTubeSites=('peertube.mastodon.host','open.tube','share.tube','tube.tr4sk.me','videos.elbinario.net','hkvideo.live','peertube.snargol.com','tube.22decembre.eu','tube.fabrigli.fr','libretube.net','libre.video','peertube.linuxrocks.online','spacepub.space','video.ploud.jp','video.omniatv.com','peertube.servebeer.com','tube.tchncs.de','tubee.fr','video.alternanet.fr','devtube.dev-wiki.de','video.samedi.pm','video.irem.univ-paris-diderot.fr','peertube.openstreetmap.fr','video.antopie.org','scitech.video','tube.4aem.com','video.ploud.fr','peervideo.net','video.valme.io','videos.pair2jeux.tube','vault.mle.party','hostyour.tv','diode.zone','visionon.tv','artitube.artifaille.fr','peertube.fr','peertube.live','tube.ac-lyon.fr','www.yiny.org','betamax.video','tube.piweb.be','pe.ertu.be','peertube.social','videos.lescommuns.org','peertube.nogafa.org','skeptikon.fr','video.tedomum.net','tube.p2p.legal','sikke.fi','exode.me','peertube.video')
  1771. for site in peerTubeSites:
  1772. if '"https://'+site in content:
  1773. url=content.split('"https://'+site)[1]
  1774. if '"' in url:
  1775. url=url.split('"')[0].replace('/watch/','/embed/')
  1776. content=content+"<center><iframe loading=\"lazy\" sandbox=\"allow-same-origin allow-scripts\" src=\"https://"+site+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1777. return content
  1778. return content
  1779. def addEmbeddedElements(translate: {},content: str) -> str:
  1780. """Adds embedded elements for various media types
  1781. """
  1782. content=addEmbeddedVideoFromSites(translate,content)
  1783. content=addEmbeddedAudio(translate,content)
  1784. return addEmbeddedVideo(translate,content)
  1785. def followerApprovalActive(baseDir: str,nickname: str,domain: str) -> bool:
  1786. """Returns true if the given account requires follower approval
  1787. """
  1788. manuallyApprovesFollowers=False
  1789. actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
  1790. if os.path.isfile(actorFilename):
  1791. actorJson=loadJson(actorFilename)
  1792. if actorJson:
  1793. if actorJson.get('manuallyApprovesFollowers'):
  1794. manuallyApprovesFollowers=actorJson['manuallyApprovesFollowers']
  1795. return manuallyApprovesFollowers
  1796. def insertQuestion(baseDir: str,translate: {}, \
  1797. nickname: str,domain: str,port: int, \
  1798. content: str, \
  1799. postJsonObject: {},pageNumber: int) -> str:
  1800. """ Inserts question selection into a post
  1801. """
  1802. if not isQuestion(postJsonObject):
  1803. return content
  1804. if len(postJsonObject['object']['oneOf'])==0:
  1805. return content
  1806. messageId=postJsonObject['id'].replace('/activity','')
  1807. if '#' in messageId:
  1808. messageId=messageId.split('#',1)[0]
  1809. pageNumberStr=''
  1810. if pageNumber:
  1811. pageNumberStr='?page='+str(pageNumber)
  1812. votesFilename= \
  1813. baseDir+'/accounts/'+nickname+'@'+domain+'/questions.txt'
  1814. showQuestionResults=False
  1815. if os.path.isfile(votesFilename):
  1816. if messageId in open(votesFilename).read():
  1817. showQuestionResults=True
  1818. if not showQuestionResults:
  1819. # show the question options
  1820. content+='<div class="question">'
  1821. content+='<form method="POST" action="/users/'+nickname+'/question'+pageNumberStr+'">'
  1822. content+='<input type="hidden" name="messageId" value="'+messageId+'"><br>'
  1823. for choice in postJsonObject['object']['oneOf']:
  1824. if not choice.get('type'):
  1825. continue
  1826. if not choice.get('name'):
  1827. continue
  1828. content+='<input type="radio" name="answer" value="'+choice['name']+'"> '+choice['name']+'<br><br>'
  1829. content+='<input type="submit" value="'+translate['Vote']+'" class="vote"><br><br>'
  1830. content+='</form></div>'
  1831. else:
  1832. # show the responses to a question
  1833. content+='<div class="questionresult">'
  1834. # get the maximum number of votes
  1835. maxVotes=1
  1836. for questionOption in postJsonObject['object']['oneOf']:
  1837. if not questionOption.get('name'):
  1838. continue
  1839. if not questionOption.get('replies'):
  1840. continue
  1841. votes=0
  1842. try:
  1843. votes=int(questionOption['replies']['totalItems'])
  1844. except:
  1845. pass
  1846. if votes>maxVotes:
  1847. maxVotes=int(votes+1)
  1848. # show the votes as sliders
  1849. questionCtr=1
  1850. for questionOption in postJsonObject['object']['oneOf']:
  1851. if not questionOption.get('name'):
  1852. continue
  1853. if not questionOption.get('replies'):
  1854. continue
  1855. votes=0
  1856. try:
  1857. votes=int(questionOption['replies']['totalItems'])
  1858. except:
  1859. pass
  1860. votesPercent=str(int(votes*100/maxVotes))
  1861. content+='<p><input type="text" title="'+str(votes)+'" name="skillName'+str(questionCtr)+'" value="'+questionOption['name']+' ('+str(votes)+')" style="width:40%">'
  1862. content+='<input type="range" min="1" max="100" class="slider" title="'+str(votes)+'" name="skillValue'+str(questionCtr)+'" value="'+votesPercent+'"></p>'
  1863. questionCtr+=1
  1864. content+='</div>'
  1865. return content
  1866. def addEmojiToDisplayName(baseDir: str,httpPrefix: str, \
  1867. nickname: str,domain: str, \
  1868. displayName: str,inProfileName: bool) -> str:
  1869. """Adds emoji icons to display names on individual posts
  1870. """
  1871. if ':' not in displayName:
  1872. return displayName
  1873. displayName=displayName.replace('<p>','').replace('</p>','')
  1874. emojiTags={}
  1875. print('TAG: displayName before tags: '+displayName)
  1876. displayName= \
  1877. addHtmlTags(baseDir,httpPrefix, \
  1878. nickname,domain,displayName,[],emojiTags)
  1879. displayName=displayName.replace('<p>','').replace('</p>','')
  1880. print('TAG: displayName after tags: '+displayName)
  1881. # convert the emoji dictionary to a list
  1882. emojiTagsList=[]
  1883. for tagName,tag in emojiTags.items():
  1884. emojiTagsList.append(tag)
  1885. print('TAG: emoji tags list: '+str(emojiTagsList))
  1886. if not inProfileName:
  1887. displayName=replaceEmojiFromTags(displayName,emojiTagsList,'post header')
  1888. else:
  1889. displayName=replaceEmojiFromTags(displayName,emojiTagsList,'profile')
  1890. print('TAG: displayName after tags 2: '+displayName)
  1891. # remove any stray emoji
  1892. while ':' in displayName:
  1893. if '://' in displayName:
  1894. break
  1895. emojiStr=displayName.split(':')[1]
  1896. prevDisplayName=displayName
  1897. displayName=displayName.replace(':'+emojiStr+':','').strip()
  1898. if prevDisplayName==displayName:
  1899. break
  1900. print('TAG: displayName after tags 3: '+displayName)
  1901. print('TAG: displayName after tag replacements: '+displayName)
  1902. return displayName
  1903. def postContainsPublic(postJsonObject: {}) -> bool:
  1904. """Does the given post contain #Public
  1905. """
  1906. containsPublic=False
  1907. if not postJsonObject['object'].get('to'):
  1908. return containsPublic
  1909. for toAddress in postJsonObject['object']['to']:
  1910. if toAddress.endswith('#Public'):
  1911. containsPublic=True
  1912. break
  1913. if not containsPublic:
  1914. if postJsonObject['object'].get('cc'):
  1915. for toAddress in postJsonObject['object']['cc']:
  1916. if toAddress.endswith('#Public'):
  1917. containsPublic=True
  1918. break
  1919. return containsPublic
  1920. def loadIndividualPostAsHtmlFromCache(baseDir: str,nickname: str,domain: str, \
  1921. postJsonObject: {}) -> str:
  1922. """If a cached html version of the given post exists then load it and
  1923. return the html text
  1924. This is much quicker than generating the html from the json object
  1925. """
  1926. cachedPostFilename=getCachedPostFilename(baseDir,nickname,domain,postJsonObject)
  1927. postHtml=''
  1928. if not cachedPostFilename:
  1929. return postHtml
  1930. if not os.path.isfile(cachedPostFilename):
  1931. return postHtml
  1932. tries=0
  1933. while tries<3:
  1934. try:
  1935. with open(cachedPostFilename, 'r') as file:
  1936. postHtml = file.read()
  1937. break
  1938. except Exception as e:
  1939. print(e)
  1940. # no sleep
  1941. tries+=1
  1942. if postHtml:
  1943. return postHtml
  1944. def saveIndividualPostAsHtmlToCache(baseDir: str,nickname: str,domain: str, \
  1945. postJsonObject: {},postHtml: str) -> bool:
  1946. """Saves the given html for a post to a cache file
  1947. This is so that it can be quickly reloaded on subsequent refresh of the timeline
  1948. """
  1949. htmlPostCacheDir=getCachedPostDirectory(baseDir,nickname,domain)
  1950. cachedPostFilename=getCachedPostFilename(baseDir,nickname,domain,postJsonObject)
  1951. # create the cache directory if needed
  1952. if not os.path.isdir(htmlPostCacheDir):
  1953. os.mkdir(htmlPostCacheDir)
  1954. try:
  1955. with open(cachedPostFilename, 'w') as fp:
  1956. fp.write(postHtml)
  1957. return True
  1958. except Exception as e:
  1959. print('ERROR: saving post to cache '+str(e))
  1960. return False
  1961. def preparePostFromHtmlCache(postHtml: str,boxName: str,pageNumber: int) -> str:
  1962. """Sets the page number on a cached html post
  1963. """
  1964. # if on the bookmarks timeline then remain there
  1965. if boxName=='tlbookmarks':
  1966. postHtml=postHtml.replace('?tl=inbox','?tl=tlbookmarks')
  1967. return postHtml.replace(';-999;',';'+str(pageNumber)+';').replace('?page=-999','?page='+str(pageNumber))
  1968. def postIsMuted(baseDir: str,nickname: str,domain: str, postJsonObject: {},messageId: str) -> bool:
  1969. """ Returns true if the given post is muted
  1970. """
  1971. isMuted=postJsonObject.get('muted')
  1972. if isMuted==True or isMuted==False:
  1973. return isMuted
  1974. postDir=baseDir+'/accounts/'+nickname+'@'+domain
  1975. muteFilename=postDir+'/inbox/'+messageId.replace('/','#')+'.json.muted'
  1976. if os.path.isfile(muteFilename):
  1977. return True
  1978. muteFilename=postDir+'/outbox/'+messageId.replace('/','#')+'.json.muted'
  1979. if os.path.isfile(muteFilename):
  1980. return True
  1981. muteFilename=baseDir+'/accounts/cache/announce/'+nickname+'/'+messageId.replace('/','#')+'.json.muted'
  1982. if os.path.isfile(muteFilename):
  1983. return True
  1984. return False
  1985. def individualPostAsHtml(recentPostsCache: {},maxRecentPosts: int, \
  1986. iconsDir: str,translate: {}, \
  1987. pageNumber: int,baseDir: str, \
  1988. session,wfRequest: {},personCache: {}, \
  1989. nickname: str,domain: str,port: int, \
  1990. postJsonObject: {}, \
  1991. avatarUrl: str,showAvatarOptions: bool,
  1992. allowDeletion: bool, \
  1993. httpPrefix: str,projectVersion: str, \
  1994. boxName: str,showRepeats=True, \
  1995. showIcons=False, \
  1996. manuallyApprovesFollowers=False, \
  1997. showPublicOnly=False,
  1998. storeToCache=True) -> str:
  1999. """ Shows a single post as html
  2000. """
  2001. postActor=postJsonObject['actor']
  2002. # ZZZzzz
  2003. if isPersonSnoozed(baseDir,nickname,domain,postActor):
  2004. return ''
  2005. avatarPosition=''
  2006. messageId=''
  2007. if postJsonObject.get('id'):
  2008. messageId=postJsonObject['id'].replace('/activity','')
  2009. messageIdStr=''
  2010. if messageId:
  2011. messageIdStr=';'+messageId
  2012. fullDomain=domain
  2013. if port:
  2014. if port!=80 and port!=443:
  2015. if ':' not in domain:
  2016. fullDomain=domain+':'+str(port)
  2017. pageNumberParam=''
  2018. if pageNumber:
  2019. pageNumberParam='?page='+str(pageNumber)
  2020. if not showPublicOnly and storeToCache and boxName!='tlmedia':
  2021. # update avatar if needed
  2022. if not avatarUrl:
  2023. avatarUrl=getPersonAvatarUrl(baseDir,postActor,personCache)
  2024. updateAvatarImageCache(session,baseDir,httpPrefix,postActor,avatarUrl,personCache)
  2025. postHtml= \
  2026. loadIndividualPostAsHtmlFromCache(baseDir,nickname,domain, \
  2027. postJsonObject)
  2028. if postHtml:
  2029. postHtml=preparePostFromHtmlCache(postHtml,boxName,pageNumber)
  2030. updateRecentPostsCache(recentPostsCache,maxRecentPosts, \
  2031. postJsonObject,postHtml)
  2032. return postHtml
  2033. if not avatarUrl:
  2034. avatarUrl=getPersonAvatarUrl(baseDir,postActor,personCache)
  2035. avatarUrl=updateAvatarImageCache(session,baseDir,httpPrefix,postActor,avatarUrl,personCache)
  2036. else:
  2037. updateAvatarImageCache(session,baseDir,httpPrefix,postActor,avatarUrl,personCache)
  2038. if not avatarUrl:
  2039. avatarUrl=postActor+'/avatar.png'
  2040. if fullDomain not in postActor:
  2041. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,displayName = \
  2042. getPersonBox(baseDir,session,wfRequest,personCache, \
  2043. projectVersion,httpPrefix,nickname,domain,'outbox')
  2044. if avatarUrl2:
  2045. avatarUrl=avatarUrl2
  2046. if displayName:
  2047. if ':' in displayName:
  2048. displayName= \
  2049. addEmojiToDisplayName(baseDir,httpPrefix, \
  2050. nickname,domain, \
  2051. displayName,False)
  2052. titleStr=displayName+' '+titleStr
  2053. avatarLink=' <a href="'+postActor+'">'
  2054. avatarLink+=' <img loading="lazy" src="'+avatarUrl+'" title="'+translate['Show profile']+'" alt=" "'+avatarPosition+'/></a>'
  2055. if showAvatarOptions and fullDomain+'/users/'+nickname not in postActor:
  2056. avatarLink=' <a href="/users/'+nickname+'?options='+postActor+';'+str(pageNumber)+';'+avatarUrl+messageIdStr+'">'
  2057. avatarLink+=' <img loading="lazy" title="'+translate['Show options for this person']+'" src="'+avatarUrl+'" '+avatarPosition+'/></a>'
  2058. avatarImageInPost=' <div class="timeline-avatar">'+avatarLink+'</div>'
  2059. # don't create new html within the bookmarks timeline
  2060. # it should already have been created for the inbox
  2061. if boxName=='tlbookmarks':
  2062. return ''
  2063. timelinePostBookmark=postJsonObject['id'].replace('/activity','').replace('://','-').replace('/','-')
  2064. # If this is the inbox timeline then don't show the repeat icon on any DMs
  2065. showRepeatIcon=showRepeats
  2066. isPublicRepeat=False
  2067. showDMicon=False
  2068. if showRepeats:
  2069. if isDM(postJsonObject):
  2070. showDMicon=True
  2071. showRepeatIcon=False
  2072. else:
  2073. if not isPublicPost(postJsonObject):
  2074. isPublicRepeat=True
  2075. titleStr=''
  2076. galleryStr=''
  2077. isAnnounced=False
  2078. if postJsonObject['type']=='Announce':
  2079. postJsonAnnounce= \
  2080. downloadAnnounce(session,baseDir,httpPrefix,nickname,domain,postJsonObject,projectVersion)
  2081. if not postJsonAnnounce:
  2082. return ''
  2083. postJsonObject=postJsonAnnounce
  2084. isAnnounced=True
  2085. if not isinstance(postJsonObject['object'], dict):
  2086. return ''
  2087. # if this post should be public then check its recipients
  2088. if showPublicOnly:
  2089. if not postContainsPublic(postJsonObject):
  2090. return ''
  2091. isModerationPost=False
  2092. if postJsonObject['object'].get('moderationStatus'):
  2093. isModerationPost=True
  2094. containerClass='container'
  2095. containerClassIcons='containericons'
  2096. timeClass='time-right'
  2097. actorNickname=getNicknameFromActor(postActor)
  2098. if not actorNickname:
  2099. # single user instance
  2100. actorNickname='dev'
  2101. actorDomain,actorPort=getDomainFromActor(postActor)
  2102. displayName=getDisplayName(baseDir,postActor,personCache)
  2103. if displayName:
  2104. if ':' in displayName:
  2105. displayName= \
  2106. addEmojiToDisplayName(baseDir,httpPrefix, \
  2107. nickname,domain, \
  2108. displayName,False)
  2109. titleStr+='<a href="/users/'+nickname+'?options='+postActor+';'+str(pageNumber)+';'+avatarUrl+messageIdStr+'">'+displayName+'</a>'
  2110. else:
  2111. if not messageId:
  2112. #pprint(postJsonObject)
  2113. print('ERROR: no messageId')
  2114. if not actorNickname:
  2115. #pprint(postJsonObject)
  2116. print('ERROR: no actorNickname')
  2117. if not actorDomain:
  2118. #pprint(postJsonObject)
  2119. print('ERROR: no actorDomain')
  2120. titleStr+='<a href="/users/'+nickname+'?options='+postActor+';'+str(pageNumber)+';'+avatarUrl+messageIdStr+'">@'+actorNickname+'@'+actorDomain+'</a>'
  2121. # Show a DM icon for DMs in the inbox timeline
  2122. if showDMicon:
  2123. titleStr=titleStr+' <img loading="lazy" src="/'+iconsDir+'/dm.png" class="DMicon"/>'
  2124. replyStr=''
  2125. if showIcons:
  2126. replyToLink=postJsonObject['object']['id']
  2127. if postJsonObject['object'].get('attributedTo'):
  2128. replyToLink+='?mention='+postJsonObject['object']['attributedTo']
  2129. if postJsonObject['object'].get('content'):
  2130. mentionedActors=getMentionsFromHtml(postJsonObject['object']['content'])
  2131. if mentionedActors:
  2132. for actorUrl in mentionedActors:
  2133. if '?mention='+actorUrl not in replyToLink:
  2134. replyToLink+='?mention='+actorUrl
  2135. if len(replyToLink)>500:
  2136. break
  2137. replyToLink+=pageNumberParam
  2138. replyStr=''
  2139. if isPublicRepeat:
  2140. replyStr+= \
  2141. '<a href="/users/'+nickname+'?replyto='+replyToLink+ \
  2142. '?actor='+postJsonObject['actor']+ \
  2143. '" title="'+translate['Reply to this post']+'">'
  2144. else:
  2145. if isDM(postJsonObject):
  2146. replyStr+= \
  2147. '<a href="/users/'+nickname+'?replydm='+replyToLink+ \
  2148. '?actor='+postJsonObject['actor']+ \
  2149. '" title="'+translate['Reply to this post']+'">'
  2150. else:
  2151. replyStr+= \
  2152. '<a href="/users/'+nickname+'?replyfollowers='+replyToLink+ \
  2153. '?actor='+postJsonObject['actor']+ \
  2154. '" title="'+translate['Reply to this post']+'">'
  2155. replyStr+='<img loading="lazy" title="'+translate['Reply to this post']+' |" alt="'+translate['Reply to this post']+' |" src="/'+iconsDir+'/reply.png"/></a>'
  2156. announceStr=''
  2157. if not isModerationPost and showRepeatIcon:
  2158. # don't allow announce/repeat of your own posts
  2159. announceIcon='repeat_inactive.png'
  2160. announceLink='repeat'
  2161. if not isPublicRepeat:
  2162. announceLink='repeatprivate'
  2163. announceTitle=translate['Repeat this post']
  2164. if announcedByPerson(postJsonObject,nickname,fullDomain):
  2165. announceIcon='repeat.png'
  2166. if not isPublicRepeat:
  2167. announceLink='unrepeatprivate'
  2168. announceTitle=translate['Undo the repeat']
  2169. announceStr= \
  2170. '<a href="/users/'+nickname+'?'+announceLink+'='+postJsonObject['object']['id']+pageNumberParam+ \
  2171. '?actor='+postJsonObject['actor']+ \
  2172. '?bm='+timelinePostBookmark+ \
  2173. '?tl='+boxName+'" title="'+announceTitle+'">'
  2174. announceStr+='<img loading="lazy" title="'+translate['Repeat this post']+' |" alt="'+translate['Repeat this post']+' |" src="/'+iconsDir+'/'+announceIcon+'"/></a>'
  2175. likeStr=''
  2176. if not isModerationPost:
  2177. likeIcon='like_inactive.png'
  2178. likeLink='like'
  2179. likeTitle=translate['Like this post']
  2180. if noOfLikes(postJsonObject)>0:
  2181. likeIcon='like.png'
  2182. if likedByPerson(postJsonObject,nickname,fullDomain):
  2183. likeLink='unlike'
  2184. likeTitle=translate['Undo the like']
  2185. likeStr= \
  2186. '<a href="/users/' + nickname + '?' + \
  2187. likeLink + '=' + postJsonObject['object']['id'] + pageNumberParam + \
  2188. '?actor='+postJsonObject['actor']+ \
  2189. '?bm='+timelinePostBookmark+ \
  2190. '?tl='+boxName+'" title="'+likeTitle+'">'
  2191. likeStr+='<img loading="lazy" title="'+likeTitle+' |" alt="'+likeTitle+' |" src="/'+iconsDir+'/'+likeIcon+'"/></a>'
  2192. bookmarkStr=''
  2193. if not isModerationPost:
  2194. bookmarkIcon='bookmark_inactive.png'
  2195. bookmarkLink='bookmark'
  2196. bookmarkTitle=translate['Bookmark this post']
  2197. if bookmarkedByPerson(postJsonObject,nickname,fullDomain):
  2198. bookmarkIcon='bookmark.png'
  2199. bookmarkLink='unbookmark'
  2200. bookmarkTitle=translate['Undo the bookmark']
  2201. bookmarkStr= \
  2202. '<a href="/users/' + nickname + '?' + \
  2203. bookmarkLink + '=' + postJsonObject['object']['id'] + pageNumberParam + \
  2204. '?actor='+postJsonObject['actor']+ \
  2205. '?bm='+timelinePostBookmark+ \
  2206. '?tl='+boxName+'" title="'+bookmarkTitle+'">'
  2207. bookmarkStr+='<img loading="lazy" title="'+bookmarkTitle+' |" alt="'+bookmarkTitle+' |" src="/'+iconsDir+'/'+bookmarkIcon+'"/></a>'
  2208. isMuted=postIsMuted(baseDir,nickname,domain,postJsonObject,messageId)
  2209. deleteStr=''
  2210. muteStr=''
  2211. if allowDeletion or \
  2212. ('/'+fullDomain+'/' in postActor and \
  2213. messageId.startswith(postActor)):
  2214. if '/users/'+nickname+'/' in messageId:
  2215. deleteStr='<a href="/users/'+nickname+'?delete='+messageId+pageNumberParam+'" title="'+translate['Delete this post']+'">'
  2216. deleteStr+='<img loading="lazy" alt="'+translate['Delete this post']+' |" title="'+translate['Delete this post']+' |" src="/'+iconsDir+'/delete.png"/></a>'
  2217. else:
  2218. if not isMuted:
  2219. muteStr='<a href="/users/'+nickname+'?mute='+messageId+pageNumberParam+'?tl='+boxName+'?bm='+timelinePostBookmark+'" title="'+translate['Mute this post']+'">'
  2220. muteStr+='<img loading="lazy" alt="'+translate['Mute this post']+' |" title="'+translate['Mute this post']+' |" src="/'+iconsDir+'/mute.png"/></a>'
  2221. else:
  2222. muteStr='<a href="/users/'+nickname+'?unmute='+messageId+pageNumberParam+'?tl='+boxName+'?bm='+timelinePostBookmark+'" title="'+translate['Undo mute']+'">'
  2223. muteStr+='<img loading="lazy" alt="'+translate['Undo mute']+' |" title="'+translate['Undo mute']+' |" src="/'+iconsDir+'/unmute.png"/></a>'
  2224. replyAvatarImageInPost=''
  2225. if showRepeatIcon:
  2226. if isAnnounced:
  2227. if postJsonObject['object'].get('attributedTo'):
  2228. if postJsonObject['object']['attributedTo'].startswith(postActor):
  2229. titleStr+=' <img loading="lazy" title="'+translate['announces']+'" alt="'+translate['announces']+'" src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/>'
  2230. else:
  2231. announceNickname=getNicknameFromActor(postJsonObject['object']['attributedTo'])
  2232. if announceNickname:
  2233. announceDomain,announcePort=getDomainFromActor(postJsonObject['object']['attributedTo'])
  2234. getPersonFromCache(baseDir,postJsonObject['object']['attributedTo'],personCache)
  2235. announceDisplayName=getDisplayName(baseDir,postJsonObject['object']['attributedTo'],personCache)
  2236. if announceDisplayName:
  2237. if ':' in announceDisplayName:
  2238. announceDisplayName= \
  2239. addEmojiToDisplayName(baseDir,httpPrefix, \
  2240. nickname,domain, \
  2241. announceDisplayName,False)
  2242. titleStr+=' <img loading="lazy" title="'+translate['announces']+'" alt="'+translate['announces']+'" src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">'+announceDisplayName+'</a>'
  2243. # show avatar of person replied to
  2244. announceActor=postJsonObject['object']['attributedTo']
  2245. announceAvatarUrl=getPersonAvatarUrl(baseDir,announceActor,personCache)
  2246. if announceAvatarUrl:
  2247. replyAvatarImageInPost= \
  2248. '<div class="timeline-avatar-reply">' \
  2249. '<a href="/users/'+nickname+'?options='+announceActor+';'+str(pageNumber)+';'+announceAvatarUrl+messageIdStr+'">' \
  2250. '<img loading="lazy" src="'+announceAvatarUrl+'" ' \
  2251. 'title="'+translate['Show options for this person']+ \
  2252. '" alt=" "'+avatarPosition+'/></a></div>'
  2253. else:
  2254. titleStr+=' <img loading="lazy" title="'+translate['announces']+'" alt="'+translate['announces']+'" src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@'+announceNickname+'@'+announceDomain+'</a>'
  2255. else:
  2256. titleStr+=' <img loading="lazy" title="'+translate['announces']+'" alt="'+translate['announces']+'" src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@unattributed</a>'
  2257. else:
  2258. titleStr+=' <img loading="lazy" title="'+translate['announces']+'" alt="'+translate['announces']+'" src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@unattributed</a>'
  2259. else:
  2260. if postJsonObject['object'].get('inReplyTo'):
  2261. containerClassIcons='containericons darker'
  2262. containerClass='container darker'
  2263. #avatarPosition=' class="right"'
  2264. if postJsonObject['object']['inReplyTo'].startswith(postActor):
  2265. titleStr+=' <img loading="lazy" title="'+translate['replying to themselves']+'" alt="'+translate['replying to themselves']+'" src="/'+iconsDir+'/reply.png" class="announceOrReply"/>'
  2266. else:
  2267. if '/statuses/' in postJsonObject['object']['inReplyTo']:
  2268. replyActor=postJsonObject['object']['inReplyTo'].split('/statuses/')[0]
  2269. replyNickname=getNicknameFromActor(replyActor)
  2270. if replyNickname:
  2271. replyDomain,replyPort=getDomainFromActor(replyActor)
  2272. if replyNickname and replyDomain:
  2273. getPersonFromCache(baseDir,replyActor,personCache)
  2274. replyDisplayName=getDisplayName(baseDir,replyActor,personCache)
  2275. if replyDisplayName:
  2276. if ':' in replyDisplayName:
  2277. replyDisplayName= \
  2278. addEmojiToDisplayName(baseDir,httpPrefix, \
  2279. nickname,domain, \
  2280. replyDisplayName,False)
  2281. titleStr+=' <img loading="lazy" title="'+translate['replying to']+'" alt="'+translate['replying to']+'" src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">'+replyDisplayName+'</a>'
  2282. # show avatar of person replied to
  2283. replyAvatarUrl=getPersonAvatarUrl(baseDir,replyActor,personCache)
  2284. if replyAvatarUrl:
  2285. replyAvatarImageInPost='<div class="timeline-avatar-reply">'
  2286. replyAvatarImageInPost+='<a href="/users/'+nickname+'?options='+replyActor+';'+str(pageNumber)+';'+replyAvatarUrl+messageIdStr+'">'
  2287. replyAvatarImageInPost+='<img loading="lazy" src="'+replyAvatarUrl+'" '
  2288. replyAvatarImageInPost+='title="'+translate['Show profile']
  2289. replyAvatarImageInPost+='" alt=" "'+avatarPosition+'/></a></div>'
  2290. else:
  2291. titleStr+=' <img loading="lazy" title="'+translate['replying to']+'" alt="'+translate['replying to']+'" src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">@'+replyNickname+'@'+replyDomain+'</a>'
  2292. else:
  2293. titleStr+=' <img loading="lazy" title="'+translate['replying to']+'" alt="'+translate['replying to']+'" src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">@unknown</a>'
  2294. else:
  2295. postDomain=postJsonObject['object']['inReplyTo'].replace('https://','').replace('http://','').replace('dat://','').replace('i2p://','')
  2296. if '/' in postDomain:
  2297. postDomain=postDomain.split('/',1)[0]
  2298. if postDomain:
  2299. titleStr+=' <img loading="lazy" title="'+translate['replying to']+'" alt="'+translate['replying to']+'" src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">'+postDomain+'</a>'
  2300. attachmentStr=''
  2301. if postJsonObject['object'].get('attachment'):
  2302. if isinstance(postJsonObject['object']['attachment'], list):
  2303. attachmentCtr=0
  2304. attachmentStr+='<div class="media">'
  2305. for attach in postJsonObject['object']['attachment']:
  2306. if attach.get('mediaType') and attach.get('url'):
  2307. mediaType=attach['mediaType']
  2308. imageDescription=''
  2309. if attach.get('name'):
  2310. imageDescription=attach['name'].replace('"',"'")
  2311. if mediaType=='image/png' or \
  2312. mediaType=='image/jpeg' or \
  2313. mediaType=='image/gif':
  2314. if attach['url'].endswith('.png') or \
  2315. attach['url'].endswith('.jpg') or \
  2316. attach['url'].endswith('.jpeg') or \
  2317. attach['url'].endswith('.webp') or \
  2318. attach['url'].endswith('.gif'):
  2319. if attachmentCtr>0:
  2320. attachmentStr+='<br>'
  2321. if boxName=='tlmedia':
  2322. galleryStr+='<div class="gallery">\n'
  2323. if not isMuted:
  2324. galleryStr+=' <a href="'+attach['url']+'">\n'
  2325. galleryStr+=' <img loading="lazy" src="'+attach['url']+'" alt="" title="">\n'
  2326. galleryStr+=' </a>\n'
  2327. if postJsonObject['object'].get('url'):
  2328. imagePostUrl=postJsonObject['object']['url']
  2329. else:
  2330. imagePostUrl=postJsonObject['object']['id']
  2331. if imageDescription and not isMuted:
  2332. galleryStr+=' <a href="'+imagePostUrl+'" class="gallerytext"><div class="gallerytext">'+imageDescription+'</div></a>\n'
  2333. else:
  2334. galleryStr+='<label class="transparent">---</label><br>'
  2335. galleryStr+=' <div class="mediaicons">\n'
  2336. galleryStr+=' '+replyStr+announceStr+likeStr+bookmarkStr+deleteStr+muteStr+'\n'
  2337. galleryStr+=' </div>\n'
  2338. galleryStr+=' <div class="mediaavatar">\n'
  2339. galleryStr+=' '+avatarLink+'\n'
  2340. galleryStr+=' </div>\n'
  2341. galleryStr+='</div>\n'
  2342. attachmentStr+='<a href="'+attach['url']+'">'
  2343. attachmentStr+='<img loading="lazy" src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment"></a>\n'
  2344. attachmentCtr+=1
  2345. elif mediaType=='video/mp4' or \
  2346. mediaType=='video/webm' or \
  2347. mediaType=='video/ogv':
  2348. extension='.mp4'
  2349. if attach['url'].endswith('.webm'):
  2350. extension='.webm'
  2351. elif attach['url'].endswith('.ogv'):
  2352. extension='.ogv'
  2353. if attach['url'].endswith(extension):
  2354. if attachmentCtr>0:
  2355. attachmentStr+='<br>'
  2356. if boxName=='tlmedia':
  2357. galleryStr+='<div class="gallery">\n'
  2358. if not isMuted:
  2359. galleryStr+=' <a href="'+attach['url']+'">\n'
  2360. galleryStr+=' <video width="600" height="400" controls>\n'
  2361. galleryStr+=' <source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="video/'+extension.replace('.','')+'">'
  2362. galleryStr+=translate['Your browser does not support the video tag.']
  2363. galleryStr+=' </video>\n'
  2364. galleryStr+=' </a>\n'
  2365. if postJsonObject['object'].get('url'):
  2366. videoPostUrl=postJsonObject['object']['url']
  2367. else:
  2368. videoPostUrl=postJsonObject['object']['id']
  2369. if imageDescription and not isMuted:
  2370. galleryStr+=' <a href="'+videoPostUrl+'" class="gallerytext"><div class="gallerytext">'+imageDescription+'</div></a>\n'
  2371. else:
  2372. galleryStr+='<label class="transparent">---</label><br>'
  2373. galleryStr+=' <div class="mediaicons">\n'
  2374. galleryStr+=' '+replyStr+announceStr+likeStr+bookmarkStr+deleteStr+muteStr+'\n'
  2375. galleryStr+=' </div>\n'
  2376. galleryStr+=' <div class="mediaavatar">\n'
  2377. galleryStr+=' '+avatarLink+'\n'
  2378. galleryStr+=' </div>\n'
  2379. galleryStr+='</div>\n'
  2380. attachmentStr+='<center><video width="400" height="300" controls>'
  2381. attachmentStr+='<source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="video/'+extension.replace('.','')+'">'
  2382. attachmentStr+=translate['Your browser does not support the video tag.']
  2383. attachmentStr+='</video></center>'
  2384. attachmentCtr+=1
  2385. elif mediaType=='audio/mpeg' or \
  2386. mediaType=='audio/ogg':
  2387. extension='.mp3'
  2388. if attach['url'].endswith('.ogg'):
  2389. extension='.ogg'
  2390. if attach['url'].endswith(extension):
  2391. if attachmentCtr>0:
  2392. attachmentStr+='<br>'
  2393. if boxName=='tlmedia':
  2394. galleryStr+='<div class="gallery">\n'
  2395. if not isMuted:
  2396. galleryStr+=' <a href="'+attach['url']+'">\n'
  2397. galleryStr+=' <audio controls>\n'
  2398. galleryStr+=' <source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="audio/'+extension.replace('.','')+'">'
  2399. galleryStr+=translate['Your browser does not support the audio tag.']
  2400. galleryStr+=' </audio>\n'
  2401. galleryStr+=' </a>\n'
  2402. if postJsonObject['object'].get('url'):
  2403. audioPostUrl=postJsonObject['object']['url']
  2404. else:
  2405. audioPostUrl=postJsonObject['object']['id']
  2406. if imageDescription and not isMuted:
  2407. galleryStr+=' <a href="'+audioPostUrl+'" class="gallerytext"><div class="gallerytext">'+imageDescription+'</div></a>\n'
  2408. else:
  2409. galleryStr+='<label class="transparent">---</label><br>'
  2410. galleryStr+=' <div class="mediaicons">\n'
  2411. galleryStr+=' '+replyStr+announceStr+likeStr+bookmarkStr+deleteStr+muteStr+'\n'
  2412. galleryStr+=' </div>\n'
  2413. galleryStr+=' <div class="mediaavatar">\n'
  2414. galleryStr+=' '+avatarLink+'\n'
  2415. galleryStr+=' </div>\n'
  2416. galleryStr+='</div>\n'
  2417. attachmentStr+='<center><audio controls>'
  2418. attachmentStr+='<source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="audio/'+extension.replace('.','')+'">'
  2419. attachmentStr+=translate['Your browser does not support the audio tag.']
  2420. attachmentStr+='</audio></center>'
  2421. attachmentCtr+=1
  2422. attachmentStr+='</div>'
  2423. publishedStr=''
  2424. if postJsonObject['object'].get('published'):
  2425. publishedStr=postJsonObject['object']['published']
  2426. if '.' not in publishedStr:
  2427. if '+' not in publishedStr:
  2428. datetimeObject = datetime.strptime(publishedStr,"%Y-%m-%dT%H:%M:%SZ")
  2429. else:
  2430. datetimeObject = datetime.strptime(publishedStr.split('+')[0]+'Z',"%Y-%m-%dT%H:%M:%SZ")
  2431. else:
  2432. publishedStr=publishedStr.replace('T',' ').split('.')[0]
  2433. datetimeObject = parse(publishedStr)
  2434. publishedStr=datetimeObject.strftime("%a %b %d, %H:%M")
  2435. footerStr='<a href="'+messageId+'" class="'+timeClass+'">'+publishedStr+'</a>\n'
  2436. # change the background color for DMs in inbox timeline
  2437. if showDMicon:
  2438. containerClassIcons='containericons dm'
  2439. containerClass='container dm'
  2440. if showIcons:
  2441. footerStr='<div class="'+containerClassIcons+'">'
  2442. footerStr+=replyStr+announceStr+likeStr+bookmarkStr+deleteStr+muteStr
  2443. footerStr+='<a href="'+messageId+'" class="'+timeClass+'">'+publishedStr+'</a>\n'
  2444. footerStr+='</div>'
  2445. postIsSensitive=False
  2446. if postJsonObject['object'].get('sensitive'):
  2447. # sensitive posts should have a summary
  2448. if postJsonObject['object'].get('summary'):
  2449. postIsSensitive=postJsonObject['object']['sensitive']
  2450. else:
  2451. # add a generic summary if none is provided
  2452. postJsonObject['object']['summary']=translate['Sensitive']
  2453. # add an extra line if there is a content warning, for better vertical spacing on mobile
  2454. if postIsSensitive:
  2455. footerStr='<br>'+footerStr
  2456. if not postJsonObject['object'].get('summary'):
  2457. postJsonObject['object']['summary']=''
  2458. if not postJsonObject['object'].get('content'):
  2459. return ''
  2460. objectContent=removeLongWords(postJsonObject['object']['content'],40,[])
  2461. if not postIsSensitive:
  2462. contentStr=objectContent+attachmentStr
  2463. contentStr=addEmbeddedElements(translate,contentStr)
  2464. contentStr=insertQuestion(baseDir,translate,nickname,domain,port, \
  2465. contentStr,postJsonObject,pageNumber)
  2466. else:
  2467. postID='post'+str(createPassword(8))
  2468. contentStr=''
  2469. if postJsonObject['object'].get('summary'):
  2470. contentStr+='<b>'+postJsonObject['object']['summary']+'</b> '
  2471. if isModerationPost:
  2472. containerClass='container report'
  2473. contentStr+='<button class="cwButton" onclick="showContentWarning('+"'"+postID+"'"+')">'+translate['SHOW MORE']+'</button>'
  2474. contentStr+='<div class="cwText" id="'+postID+'">'
  2475. contentStr+=objectContent+attachmentStr
  2476. contentStr=addEmbeddedElements(translate,contentStr)
  2477. contentStr=insertQuestion(baseDir,translate,nickname,domain,port, \
  2478. contentStr,postJsonObject,pageNumber)
  2479. contentStr+='</div>'
  2480. if postJsonObject['object'].get('tag'):
  2481. contentStr=replaceEmojiFromTags(contentStr,postJsonObject['object']['tag'],'content')
  2482. if isMuted:
  2483. contentStr=''
  2484. else:
  2485. contentStr='<div class="message">'+contentStr+'</div>'
  2486. postHtml=''
  2487. if boxName!='tlmedia':
  2488. postHtml='<div id="'+timelinePostBookmark+'" class="'+containerClass+'">\n'
  2489. postHtml+=avatarImageInPost
  2490. postHtml+='<p class="post-title">'+titleStr+replyAvatarImageInPost+'</p>'
  2491. postHtml+=contentStr+footerStr
  2492. postHtml+='</div>\n'
  2493. else:
  2494. postHtml=galleryStr
  2495. if not showPublicOnly and storeToCache and \
  2496. boxName!='tlmedia'and boxName!='tlbookmarks':
  2497. saveIndividualPostAsHtmlToCache(baseDir,nickname,domain, \
  2498. postJsonObject,postHtml)
  2499. updateRecentPostsCache(recentPostsCache,maxRecentPosts, \
  2500. postJsonObject,postHtml)
  2501. return postHtml
  2502. def isQuestion(postObjectJson: {}) -> bool:
  2503. """ is the given post a question?
  2504. """
  2505. if postObjectJson['type']!='Create' and \
  2506. postObjectJson['type']!='Update':
  2507. return False
  2508. if not isinstance(postObjectJson['object'], dict):
  2509. return False
  2510. if not postObjectJson['object'].get('type'):
  2511. return False
  2512. if postObjectJson['object']['type']!='Question':
  2513. return False
  2514. if not postObjectJson['object'].get('oneOf'):
  2515. return False
  2516. if not isinstance(postObjectJson['object']['oneOf'], list):
  2517. return False
  2518. return True
  2519. def htmlTimeline(defaultTimeline: str, \
  2520. recentPostsCache: {},maxRecentPosts: int, \
  2521. translate: {},pageNumber: int, \
  2522. itemsPerPage: int,session,baseDir: str, \
  2523. wfRequest: {},personCache: {}, \
  2524. nickname: str,domain: str,port: int,timelineJson: {}, \
  2525. boxName: str,allowDeletion: bool, \
  2526. httpPrefix: str,projectVersion: str, \
  2527. manuallyApproveFollowers: bool) -> str:
  2528. """Show the timeline as html
  2529. """
  2530. accountDir=baseDir+'/accounts/'+nickname+'@'+domain
  2531. # should the calendar icon be highlighted?
  2532. calendarImage='calendar.png'
  2533. calendarPath='/calendar'
  2534. calendarFile=accountDir+'/.newCalendar'
  2535. if os.path.isfile(calendarFile):
  2536. calendarImage='calendar_notify.png'
  2537. with open(calendarFile, 'r') as calfile:
  2538. calendarPath=calfile.read().replace('##sent##','').replace('\n', '')
  2539. # should the DM button be highlighted?
  2540. newDM=False
  2541. dmFile=accountDir+'/.newDM'
  2542. if os.path.isfile(dmFile):
  2543. newDM=True
  2544. if boxName=='dm':
  2545. os.remove(dmFile)
  2546. # should the Replies button be highlighted?
  2547. newReply=False
  2548. replyFile=accountDir+'/.newReply'
  2549. if os.path.isfile(replyFile):
  2550. newReply=True
  2551. if boxName=='tlreplies':
  2552. os.remove(replyFile)
  2553. # should the Shares button be highlighted?
  2554. newShare=False
  2555. newShareFile=accountDir+'/.newShare'
  2556. if os.path.isfile(newShareFile):
  2557. newShare=True
  2558. if boxName=='tlshares':
  2559. os.remove(newShareFile)
  2560. # should the Moderation button be highlighted?
  2561. newReport=False
  2562. newReportFile=accountDir+'/.newReport'
  2563. if os.path.isfile(newReportFile):
  2564. newReport=True
  2565. if boxName=='moderation':
  2566. os.remove(newReportFile)
  2567. iconsDir=getIconsDir(baseDir)
  2568. cssFilename=baseDir+'/epicyon-profile.css'
  2569. if os.path.isfile(baseDir+'/epicyon.css'):
  2570. cssFilename=baseDir+'/epicyon.css'
  2571. bannerFile='banner.png'
  2572. bannerFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+bannerFile
  2573. if not os.path.isfile(bannerFilename):
  2574. bannerFile='banner.jpg'
  2575. bannerFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+bannerFile
  2576. if not os.path.isfile(bannerFilename):
  2577. bannerFile='banner.gif'
  2578. bannerFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/'+bannerFile
  2579. if not os.path.isfile(bannerFilename):
  2580. bannerFile='banner.webp'
  2581. with open(cssFilename, 'r') as cssFile:
  2582. profileStyle = \
  2583. cssFile.read().replace('banner.png', \
  2584. '/users/'+nickname+'/'+bannerFile)
  2585. if httpPrefix!='https':
  2586. profileStyle=profileStyle.replace('https://',httpPrefix+'://')
  2587. moderator=isModerator(baseDir,nickname)
  2588. inboxButton='button'
  2589. dmButton='button'
  2590. if newDM:
  2591. dmButton='buttonhighlighted'
  2592. repliesButton='button'
  2593. if newReply:
  2594. repliesButton='buttonhighlighted'
  2595. mediaButton='button'
  2596. bookmarksButton='button'
  2597. sentButton='button'
  2598. sharesButton='button'
  2599. if newShare:
  2600. sharesButton='buttonhighlighted'
  2601. moderationButton='button'
  2602. if newReport:
  2603. moderationButton='buttonhighlighted'
  2604. if boxName=='inbox':
  2605. inboxButton='buttonselected'
  2606. elif boxName=='dm':
  2607. dmButton='buttonselected'
  2608. if newDM:
  2609. dmButton='buttonselectedhighlighted'
  2610. elif boxName=='tlreplies':
  2611. repliesButton='buttonselected'
  2612. if newReply:
  2613. repliesButton='buttonselectedhighlighted'
  2614. elif boxName=='tlmedia':
  2615. mediaButton='buttonselected'
  2616. elif boxName=='outbox':
  2617. sentButton='buttonselected'
  2618. elif boxName=='moderation':
  2619. moderationButton='buttonselected'
  2620. if newReport:
  2621. moderationButton='buttonselectedhighlighted'
  2622. elif boxName=='tlshares':
  2623. sharesButton='buttonselected'
  2624. if newShare:
  2625. sharesButton='buttonselectedhighlighted'
  2626. elif boxName=='tlbookmarks':
  2627. bookmarksButton='buttonselected'
  2628. fullDomain=domain
  2629. if port!=80 and port!=443:
  2630. if ':' not in domain:
  2631. fullDomain=domain+':'+str(port)
  2632. actor=httpPrefix+'://'+fullDomain+'/users/'+nickname
  2633. showIndividualPostIcons=True
  2634. followApprovals=''
  2635. followRequestsFilename= \
  2636. baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
  2637. if os.path.isfile(followRequestsFilename):
  2638. with open(followRequestsFilename,'r') as f:
  2639. for line in f:
  2640. if len(line)>0:
  2641. # show follow approvals icon
  2642. followApprovals='<a href="'+actor+'/followers"><img loading="lazy" class="timelineicon" alt="'+translate['Approve follow requests']+'" title="'+translate['Approve follow requests']+'" src="/'+iconsDir+'/person.png"/></a>'
  2643. break
  2644. moderationButtonStr=''
  2645. if moderator:
  2646. moderationButtonStr='<a href="'+actor+'/moderation"><button class="'+moderationButton+'"><span>'+translate['Mod']+' </span></button></a>'
  2647. sharesButtonStr='<a href="'+actor+'/tlshares"><button class="'+sharesButton+'"><span>'+translate['Shares']+' </span></button></a>'
  2648. bookmarksButtonStr='<a href="'+actor+'/tlbookmarks"><button class="'+bookmarksButton+'"><span>'+translate['Bookmarks']+' </span></button></a>'
  2649. tlStr=htmlHeader(cssFilename,profileStyle)
  2650. #if (boxName=='inbox' or boxName=='dm') and pageNumber==1:
  2651. # refresh if on the first page of the inbox and dm timeline
  2652. #tlStr=htmlHeader(cssFilename,profileStyle,240)
  2653. if boxName!='dm':
  2654. if not manuallyApproveFollowers:
  2655. newPostButtonStr='<a href="'+actor+'/newpost"><img loading="lazy" src="/'+iconsDir+'/newpost.png" title="'+translate['Create a new post']+'" alt="'+translate['Create a new post']+'" class="timelineicon"/></a>'
  2656. else:
  2657. newPostButtonStr='<a href="'+actor+'/newfollowers"><img loading="lazy" src="/'+iconsDir+'/newpost.png" title="'+translate['Create a new post']+'" alt="'+translate['Create a new post']+'" class="timelineicon"/></a>'
  2658. else:
  2659. newPostButtonStr='<a href="'+actor+'/newdm"><img loading="lazy" src="/'+iconsDir+'/newpost.png" title="'+translate['Create a new DM']+'" alt="'+translate['Create a new DM']+'" class="timelineicon"/></a>'
  2660. # This creates a link to the profile page when viewed in lynx, but should be invisible in a graphical web browser
  2661. tlStr+='<a href="/users/'+nickname+'"><label class="transparent">'+translate['Switch to profile view']+'</label></a>'
  2662. # banner and row of buttons
  2663. tlStr+='<a href="/users/'+nickname+'" title="'+translate['Switch to profile view']+'" alt="'+translate['Switch to profile view']+'">'
  2664. tlStr+='<div class="timeline-banner">'
  2665. tlStr+='</div></a>'
  2666. tlStr+='<div class="container">\n'
  2667. if defaultTimeline!='tlmedia':
  2668. tlStr+=' <a href="'+actor+'/inbox"><button class="'+inboxButton+'"><span>'+translate['Inbox']+'</span></button></a>'
  2669. else:
  2670. tlStr+=' <a href="'+actor+'/tlmedia"><button class="'+mediaButton+'"><span>'+translate['Media']+'</span></button></a>'
  2671. tlStr+=' <a href="'+actor+'/dm"><button class="'+dmButton+'"><span>'+translate['DM']+'</span></button></a>'
  2672. tlStr+=' <a href="'+actor+'/tlreplies"><button class="'+repliesButton+'"><span>'+translate['Replies']+'</span></button></a>'
  2673. if defaultTimeline!='tlmedia':
  2674. tlStr+=' <a href="'+actor+'/tlmedia"><button class="'+mediaButton+'"><span>'+translate['Media']+'</span></button></a>'
  2675. else:
  2676. tlStr+=' <a href="'+actor+'/inbox"><button class="'+inboxButton+'"><span>'+translate['Inbox']+'</span></button></a>'
  2677. tlStr+=' <a href="'+actor+'/outbox"><button class="'+sentButton+'"><span>'+translate['Outbox']+'</span></button></a>'
  2678. tlStr+=sharesButtonStr+bookmarksButtonStr+moderationButtonStr+newPostButtonStr
  2679. tlStr+=' <a href="'+actor+'/search"><img loading="lazy" src="/'+iconsDir+'/search.png" title="'+translate['Search and follow']+'" alt="'+translate['Search and follow']+'" class="timelineicon"/></a>'
  2680. tlStr+=' <a href="'+actor+calendarPath+'"><img loading="lazy" src="/'+iconsDir+'/'+calendarImage+'" title="'+translate['Calendar']+'" alt="'+translate['Calendar']+'" class="timelineicon"/></a>'
  2681. tlStr+=' <a href="'+actor+'/'+boxName+'"><img loading="lazy" src="/'+iconsDir+'/refresh.png" title="'+translate['Refresh']+'" alt="'+translate['Refresh']+'" class="timelineicon"/></a>'
  2682. tlStr+=followApprovals
  2683. tlStr+='</div>'
  2684. # second row of buttons for moderator actions
  2685. if moderator and boxName=='moderation':
  2686. tlStr+='<form method="POST" action="/users/'+nickname+'/moderationaction">'
  2687. tlStr+='<div class="container">\n'
  2688. tlStr+=' <b>'+translate['Nickname or URL. Block using *@domain or nickname@domain']+'</b><br>\n'
  2689. tlStr+=' <input type="text" name="moderationAction" value="" autofocus><br>\n'
  2690. tlStr+=' <input type="submit" title="'+translate['Remove the above item']+'" name="submitRemove" value="'+translate['Remove']+'">'
  2691. tlStr+=' <input type="submit" title="'+translate['Suspend the above account nickname']+'" name="submitSuspend" value="'+translate['Suspend']+'">'
  2692. tlStr+=' <input type="submit" title="'+translate['Remove a suspension for an account nickname']+'" name="submitUnsuspend" value="'+translate['Unsuspend']+'">'
  2693. tlStr+=' <input type="submit" title="'+translate['Block an account on another instance']+'" name="submitBlock" value="'+translate['Block']+'">'
  2694. tlStr+=' <input type="submit" title="'+translate['Unblock an account on another instance']+'" name="submitUnblock" value="'+translate['Unblock']+'">'
  2695. tlStr+=' <input type="submit" title="'+translate['Information about current blocks/suspensions']+'" name="submitInfo" value="'+translate['Info']+'">'
  2696. tlStr+='</div></form>'
  2697. if boxName=='tlshares':
  2698. maxSharesPerAccount=itemsPerPage
  2699. return tlStr+ \
  2700. htmlSharesTimeline(translate,pageNumber,itemsPerPage, \
  2701. baseDir,actor,nickname,domain,port, \
  2702. maxSharesPerAccount,httpPrefix) + \
  2703. htmlFooter()
  2704. # add the javascript for content warnings
  2705. tlStr+='<script>'+contentWarningScript()+'</script>'
  2706. # page up arrow
  2707. if pageNumber>1:
  2708. tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber-1)+'"><img loading="lazy" class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"></a></center>'
  2709. # show the posts
  2710. itemCtr=0
  2711. if timelineJson:
  2712. if boxName=='tlmedia':
  2713. if pageNumber>1:
  2714. tlStr+='<br>'
  2715. tlStr+='<div class="galleryContainer">\n'
  2716. for item in timelineJson['orderedItems']:
  2717. if item['type']=='Create' or item['type']=='Announce' or item['type']=='Update':
  2718. # is the actor who sent this post snoozed?
  2719. if isPersonSnoozed(baseDir,nickname,domain,item['actor']):
  2720. continue
  2721. # is the post in the memory cache of recent ones?
  2722. currTlStr=None
  2723. if boxName!='tlmedia' and recentPostsCache.get('index'):
  2724. postId=item['id'].replace('/activity','').replace('/','#')
  2725. if postId in recentPostsCache['index']:
  2726. if not item.get('muted'):
  2727. if recentPostsCache['html'].get(postId):
  2728. currTlStr=recentPostsCache['html'][postId]
  2729. currTlStr= \
  2730. preparePostFromHtmlCache(currTlStr,boxName,pageNumber)
  2731. if not currTlStr:
  2732. # read the post from disk
  2733. currTlStr= \
  2734. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  2735. iconsDir,translate,pageNumber, \
  2736. baseDir,session,wfRequest,personCache, \
  2737. nickname,domain,port,item,None,True, \
  2738. allowDeletion, \
  2739. httpPrefix,projectVersion,boxName, \
  2740. boxName!='dm', \
  2741. showIndividualPostIcons, \
  2742. manuallyApproveFollowers,False,True)
  2743. if currTlStr:
  2744. itemCtr+=1
  2745. tlStr+=currTlStr
  2746. if boxName=='tlmedia':
  2747. tlStr+='</div>\n'
  2748. # page down arrow
  2749. if itemCtr>2:
  2750. tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber+1)+'"><img loading="lazy" class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"></a></center>'
  2751. tlStr+=htmlFooter()
  2752. return tlStr
  2753. def htmlShares(defaultTimeline: str, \
  2754. recentPostsCache: {},maxRecentPosts: int, \
  2755. translate: {},pageNumber: int,itemsPerPage: int, \
  2756. session,baseDir: str,wfRequest: {},personCache: {}, \
  2757. nickname: str,domain: str,port: int, \
  2758. allowDeletion: bool, \
  2759. httpPrefix: str,projectVersion: str) -> str:
  2760. """Show the shares timeline as html
  2761. """
  2762. manuallyApproveFollowers= \
  2763. followerApprovalActive(baseDir,nickname,domain)
  2764. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2765. translate,pageNumber, \
  2766. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2767. nickname,domain,port,None,'tlshares',allowDeletion, \
  2768. httpPrefix,projectVersion,manuallyApproveFollowers)
  2769. def htmlInbox(defaultTimeline: str, \
  2770. recentPostsCache: {},maxRecentPosts: int, \
  2771. translate: {},pageNumber: int,itemsPerPage: int, \
  2772. session,baseDir: str,wfRequest: {},personCache: {}, \
  2773. nickname: str,domain: str,port: int,inboxJson: {}, \
  2774. allowDeletion: bool, \
  2775. httpPrefix: str,projectVersion: str) -> str:
  2776. """Show the inbox as html
  2777. """
  2778. manuallyApproveFollowers= \
  2779. followerApprovalActive(baseDir,nickname,domain)
  2780. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2781. translate,pageNumber, \
  2782. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2783. nickname,domain,port,inboxJson,'inbox',allowDeletion, \
  2784. httpPrefix,projectVersion,manuallyApproveFollowers)
  2785. def htmlBookmarks(defaultTimeline: str, \
  2786. recentPostsCache: {},maxRecentPosts: int, \
  2787. translate: {},pageNumber: int,itemsPerPage: int, \
  2788. session,baseDir: str,wfRequest: {},personCache: {}, \
  2789. nickname: str,domain: str,port: int,bookmarksJson: {}, \
  2790. allowDeletion: bool, \
  2791. httpPrefix: str,projectVersion: str) -> str:
  2792. """Show the bookmarks as html
  2793. """
  2794. manuallyApproveFollowers= \
  2795. followerApprovalActive(baseDir,nickname,domain)
  2796. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2797. translate,pageNumber, \
  2798. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2799. nickname,domain,port,bookmarksJson,'tlbookmarks',allowDeletion, \
  2800. httpPrefix,projectVersion,manuallyApproveFollowers)
  2801. def htmlInboxDMs(defaultTimeline: str, \
  2802. recentPostsCache: {},maxRecentPosts: int, \
  2803. translate: {},pageNumber: int,itemsPerPage: int, \
  2804. session,baseDir: str,wfRequest: {},personCache: {}, \
  2805. nickname: str,domain: str,port: int,inboxJson: {}, \
  2806. allowDeletion: bool, \
  2807. httpPrefix: str,projectVersion: str) -> str:
  2808. """Show the DM timeline as html
  2809. """
  2810. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2811. translate,pageNumber, \
  2812. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2813. nickname,domain,port,inboxJson,'dm',allowDeletion, \
  2814. httpPrefix,projectVersion,False)
  2815. def htmlInboxReplies(defaultTimeline: str, \
  2816. recentPostsCache: {},maxRecentPosts: int, \
  2817. translate: {},pageNumber: int,itemsPerPage: int, \
  2818. session,baseDir: str,wfRequest: {},personCache: {}, \
  2819. nickname: str,domain: str,port: int,inboxJson: {}, \
  2820. allowDeletion: bool, \
  2821. httpPrefix: str,projectVersion: str) -> str:
  2822. """Show the replies timeline as html
  2823. """
  2824. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2825. translate,pageNumber, \
  2826. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2827. nickname,domain,port,inboxJson,'tlreplies',allowDeletion, \
  2828. httpPrefix,projectVersion,False)
  2829. def htmlInboxMedia(defaultTimeline: str, \
  2830. recentPostsCache: {},maxRecentPosts: int, \
  2831. translate: {},pageNumber: int,itemsPerPage: int, \
  2832. session,baseDir: str,wfRequest: {},personCache: {}, \
  2833. nickname: str,domain: str,port: int,inboxJson: {}, \
  2834. allowDeletion: bool, \
  2835. httpPrefix: str,projectVersion: str) -> str:
  2836. """Show the media timeline as html
  2837. """
  2838. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2839. translate,pageNumber, \
  2840. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2841. nickname,domain,port,inboxJson,'tlmedia',allowDeletion, \
  2842. httpPrefix,projectVersion,False)
  2843. def htmlModeration(defaultTimeline: str, \
  2844. recentPostsCache: {},maxRecentPosts: int, \
  2845. translate: {},pageNumber: int,itemsPerPage: int, \
  2846. session,baseDir: str,wfRequest: {},personCache: {}, \
  2847. nickname: str,domain: str,port: int,inboxJson: {}, \
  2848. allowDeletion: bool, \
  2849. httpPrefix: str,projectVersion: str) -> str:
  2850. """Show the moderation feed as html
  2851. """
  2852. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2853. translate,pageNumber, \
  2854. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2855. nickname,domain,port,inboxJson,'moderation',allowDeletion, \
  2856. httpPrefix,projectVersion,True)
  2857. def htmlOutbox(defaultTimeline: str, \
  2858. recentPostsCache: {},maxRecentPosts: int, \
  2859. translate: {},pageNumber: int,itemsPerPage: int, \
  2860. session,baseDir: str,wfRequest: {},personCache: {}, \
  2861. nickname: str,domain: str,port: int,outboxJson: {}, \
  2862. allowDeletion: bool,
  2863. httpPrefix: str,projectVersion: str) -> str:
  2864. """Show the Outbox as html
  2865. """
  2866. manuallyApproveFollowers= \
  2867. followerApprovalActive(baseDir,nickname,domain)
  2868. return htmlTimeline(defaultTimeline,recentPostsCache,maxRecentPosts, \
  2869. translate,pageNumber, \
  2870. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2871. nickname,domain,port,outboxJson,'outbox',allowDeletion, \
  2872. httpPrefix,projectVersion,manuallyApproveFollowers)
  2873. def htmlIndividualPost(recentPostsCache: {},maxRecentPosts: int, \
  2874. translate: {}, \
  2875. baseDir: str,session,wfRequest: {},personCache: {}, \
  2876. nickname: str,domain: str,port: int,authorized: bool, \
  2877. postJsonObject: {},httpPrefix: str,projectVersion: str) -> str:
  2878. """Show an individual post as html
  2879. """
  2880. iconsDir=getIconsDir(baseDir)
  2881. postStr='<script>'+contentWarningScript()+'</script>'
  2882. postStr+= \
  2883. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  2884. iconsDir,translate,None, \
  2885. baseDir,session,wfRequest,personCache, \
  2886. nickname,domain,port,postJsonObject,None,True,False, \
  2887. httpPrefix,projectVersion,'inbox', \
  2888. False,authorized,False,False,False)
  2889. messageId=postJsonObject['id'].replace('/activity','')
  2890. # show the previous posts
  2891. if isinstance(postJsonObject['object'], dict):
  2892. while postJsonObject['object'].get('inReplyTo'):
  2893. postFilename=locatePost(baseDir,nickname,domain,postJsonObject['object']['inReplyTo'])
  2894. if not postFilename:
  2895. break
  2896. postJsonObject=loadJson(postFilename)
  2897. if postJsonObject:
  2898. postStr= \
  2899. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  2900. iconsDir,translate,None, \
  2901. baseDir,session,wfRequest,personCache, \
  2902. nickname,domain,port,postJsonObject, \
  2903. None,True,False, \
  2904. httpPrefix,projectVersion,'inbox', \
  2905. False,authorized,False,False,False)+postStr
  2906. # show the following posts
  2907. postFilename=locatePost(baseDir,nickname,domain,messageId)
  2908. if postFilename:
  2909. # is there a replies file for this post?
  2910. repliesFilename=postFilename.replace('.json','.replies')
  2911. if os.path.isfile(repliesFilename):
  2912. # get items from the replies file
  2913. repliesJson={'orderedItems': []}
  2914. populateRepliesJson(baseDir,nickname,domain,repliesFilename,authorized,repliesJson)
  2915. # add items to the html output
  2916. for item in repliesJson['orderedItems']:
  2917. postStr+= \
  2918. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  2919. iconsDir,translate,None, \
  2920. baseDir,session,wfRequest,personCache, \
  2921. nickname,domain,port,item,None,True,False, \
  2922. httpPrefix,projectVersion,'inbox', \
  2923. False,authorized,False,False,False)
  2924. cssFilename=baseDir+'/epicyon-profile.css'
  2925. if os.path.isfile(baseDir+'/epicyon.css'):
  2926. cssFilename=baseDir+'/epicyon.css'
  2927. with open(cssFilename, 'r') as cssFile:
  2928. postsCSS=cssFile.read()
  2929. if httpPrefix!='https':
  2930. postsCSS=postsCSS.replace('https://',httpPrefix+'://')
  2931. return htmlHeader(cssFilename,postsCSS)+postStr+htmlFooter()
  2932. def htmlPostReplies(recentPostsCache: {},maxRecentPosts: int, \
  2933. translate: {},baseDir: str, \
  2934. session,wfRequest: {},personCache: {}, \
  2935. nickname: str,domain: str,port: int,repliesJson: {}, \
  2936. httpPrefix: str,projectVersion: str) -> str:
  2937. """Show the replies to an individual post as html
  2938. """
  2939. iconsDir=getIconsDir(baseDir)
  2940. repliesStr=''
  2941. if repliesJson.get('orderedItems'):
  2942. for item in repliesJson['orderedItems']:
  2943. repliesStr+= \
  2944. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  2945. iconsDir,translate,None, \
  2946. baseDir,session,wfRequest,personCache, \
  2947. nickname,domain,port,item,None,True,False, \
  2948. httpPrefix,projectVersion,'inbox', \
  2949. False,False,False,False,False)
  2950. cssFilename=baseDir+'/epicyon-profile.css'
  2951. if os.path.isfile(baseDir+'/epicyon.css'):
  2952. cssFilename=baseDir+'/epicyon.css'
  2953. with open(cssFilename, 'r') as cssFile:
  2954. postsCSS=cssFile.read()
  2955. if httpPrefix!='https':
  2956. postsCSS=postsCSS.replace('https://',httpPrefix+'://')
  2957. return htmlHeader(cssFilename,postsCSS)+repliesStr+htmlFooter()
  2958. def htmlRemoveSharedItem(translate: {},baseDir: str,actor: str,shareName: str) -> str:
  2959. """Shows a screen asking to confirm the removal of a shared item
  2960. """
  2961. itemID=getValidSharedItemID(shareName)
  2962. nickname=getNicknameFromActor(actor)
  2963. domain,port=getDomainFromActor(actor)
  2964. sharesFile=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json'
  2965. if not os.path.isfile(sharesFile):
  2966. print('ERROR: no shares file '+sharesFile)
  2967. return None
  2968. sharesJson=loadJson(sharesFile)
  2969. if not sharesJson:
  2970. print('ERROR: unable to load shares.json')
  2971. return None
  2972. if not sharesJson.get(itemID):
  2973. print('ERROR: share named "'+itemID+'" is not in '+sharesFile)
  2974. return None
  2975. sharedItemDisplayName=sharesJson[itemID]['displayName']
  2976. sharedItemImageUrl=None
  2977. if sharesJson[itemID].get('imageUrl'):
  2978. sharedItemImageUrl=sharesJson[itemID]['imageUrl']
  2979. if os.path.isfile(baseDir+'/img/shares-background.png'):
  2980. if not os.path.isfile(baseDir+'/accounts/shares-background.png'):
  2981. copyfile(baseDir+'/img/shares-background.png',baseDir+'/accounts/shares-background.png')
  2982. cssFilename=baseDir+'/epicyon-follow.css'
  2983. if os.path.isfile(baseDir+'/follow.css'):
  2984. cssFilename=baseDir+'/follow.css'
  2985. with open(cssFilename, 'r') as cssFile:
  2986. profileStyle = cssFile.read()
  2987. sharesStr=htmlHeader(cssFilename,profileStyle)
  2988. sharesStr+='<div class="follow">'
  2989. sharesStr+=' <div class="followAvatar">'
  2990. sharesStr+=' <center>'
  2991. if sharedItemImageUrl:
  2992. sharesStr+=' <img loading="lazy" src="'+sharedItemImageUrl+'"/>'
  2993. sharesStr+=' <p class="followText">'+translate['Remove']+' '+sharedItemDisplayName+' ?</p>'
  2994. sharesStr+=' <form method="POST" action="'+actor+'/rmshare">'
  2995. sharesStr+=' <input type="hidden" name="actor" value="'+actor+'">'
  2996. sharesStr+=' <input type="hidden" name="shareName" value="'+shareName+'">'
  2997. sharesStr+=' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>'
  2998. sharesStr+=' <a href="'+actor+'/inbox'+'"><button class="button">'+translate['No']+'</button></a>'
  2999. sharesStr+=' </form>'
  3000. sharesStr+=' </center>'
  3001. sharesStr+=' </div>'
  3002. sharesStr+='</div>'
  3003. sharesStr+=htmlFooter()
  3004. return sharesStr
  3005. def htmlDeletePost(recentPostsCache: {},maxRecentPosts: int, \
  3006. translate,pageNumber: int, \
  3007. session,baseDir: str,messageId: str, \
  3008. httpPrefix: str,projectVersion: str, \
  3009. wfRequest: {},personCache: {}) -> str:
  3010. """Shows a screen asking to confirm the deletion of a post
  3011. """
  3012. if '/statuses/' not in messageId:
  3013. return None
  3014. iconsDir=getIconsDir(baseDir)
  3015. actor=messageId.split('/statuses/')[0]
  3016. nickname=getNicknameFromActor(actor)
  3017. domain,port=getDomainFromActor(actor)
  3018. postFilename=locatePost(baseDir,nickname,domain,messageId)
  3019. if not postFilename:
  3020. return None
  3021. postJsonObject=loadJson(postFilename)
  3022. if not postJsonObject:
  3023. return None
  3024. if os.path.isfile(baseDir+'/img/delete-background.png'):
  3025. if not os.path.isfile(baseDir+'/accounts/delete-background.png'):
  3026. copyfile(baseDir+'/img/delete-background.png', \
  3027. baseDir+'/accounts/delete-background.png')
  3028. deletePostStr=None
  3029. cssFilename=baseDir+'/epicyon-profile.css'
  3030. if os.path.isfile(baseDir+'/epicyon.css'):
  3031. cssFilename=baseDir+'/epicyon.css'
  3032. with open(cssFilename, 'r') as cssFile:
  3033. profileStyle = cssFile.read()
  3034. if httpPrefix!='https':
  3035. profileStyle=profileStyle.replace('https://',httpPrefix+'://')
  3036. deletePostStr=htmlHeader(cssFilename,profileStyle)
  3037. deletePostStr+='<script>'+contentWarningScript()+'</script>'
  3038. deletePostStr+= \
  3039. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  3040. iconsDir,translate,pageNumber, \
  3041. baseDir,session,wfRequest,personCache, \
  3042. nickname,domain,port,postJsonObject, \
  3043. None,True,False, \
  3044. httpPrefix,projectVersion,'outbox', \
  3045. False,False,False,False,False)
  3046. deletePostStr+='<center>'
  3047. deletePostStr+=' <p class="followText">'+translate['Delete this post?']+'</p>'
  3048. deletePostStr+=' <form method="POST" action="'+actor+'/rmpost">'
  3049. deletePostStr+=' <input type="hidden" name="pageNumber" value="'+str(pageNumber)+'">'
  3050. deletePostStr+=' <input type="hidden" name="messageId" value="'+messageId+'">'
  3051. deletePostStr+=' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>'
  3052. deletePostStr+=' <a href="'+actor+'/inbox'+'"><button class="button">'+translate['No']+'</button></a>'
  3053. deletePostStr+=' </form>'
  3054. deletePostStr+='</center>'
  3055. deletePostStr+=htmlFooter()
  3056. return deletePostStr
  3057. def htmlFollowConfirm(translate: {},baseDir: str, \
  3058. originPathStr: str, \
  3059. followActor: str, \
  3060. followProfileUrl: str) -> str:
  3061. """Asks to confirm a follow
  3062. """
  3063. followDomain,port=getDomainFromActor(followActor)
  3064. if os.path.isfile(baseDir+'/img/follow-background.png'):
  3065. if not os.path.isfile(baseDir+'/accounts/follow-background.png'):
  3066. copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png')
  3067. cssFilename=baseDir+'/epicyon-follow.css'
  3068. if os.path.isfile(baseDir+'/follow.css'):
  3069. cssFilename=baseDir+'/follow.css'
  3070. with open(cssFilename, 'r') as cssFile:
  3071. profileStyle = cssFile.read()
  3072. followStr=htmlHeader(cssFilename,profileStyle)
  3073. followStr+='<div class="follow">'
  3074. followStr+=' <div class="followAvatar">'
  3075. followStr+=' <center>'
  3076. followStr+=' <a href="'+followActor+'">'
  3077. followStr+=' <img loading="lazy" src="'+followProfileUrl+'"/></a>'
  3078. followStr+=' <p class="followText">'+translate['Follow']+' '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
  3079. followStr+=' <form method="POST" action="'+originPathStr+'/followconfirm">'
  3080. followStr+=' <input type="hidden" name="actor" value="'+followActor+'">'
  3081. followStr+=' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>'
  3082. followStr+=' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>'
  3083. followStr+=' </form>'
  3084. followStr+='</center>'
  3085. followStr+='</div>'
  3086. followStr+='</div>'
  3087. followStr+=htmlFooter()
  3088. return followStr
  3089. def htmlUnfollowConfirm(translate: {},baseDir: str, \
  3090. originPathStr: str, \
  3091. followActor: str, \
  3092. followProfileUrl: str) -> str:
  3093. """Asks to confirm unfollowing an actor
  3094. """
  3095. followDomain,port=getDomainFromActor(followActor)
  3096. if os.path.isfile(baseDir+'/img/follow-background.png'):
  3097. if not os.path.isfile(baseDir+'/accounts/follow-background.png'):
  3098. copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png')
  3099. cssFilename=baseDir+'/epicyon-follow.css'
  3100. if os.path.isfile(baseDir+'/follow.css'):
  3101. cssFilename=baseDir+'/follow.css'
  3102. with open(cssFilename, 'r') as cssFile:
  3103. profileStyle = cssFile.read()
  3104. followStr=htmlHeader(cssFilename,profileStyle)
  3105. followStr+='<div class="follow">'
  3106. followStr+=' <div class="followAvatar">'
  3107. followStr+=' <center>'
  3108. followStr+=' <a href="'+followActor+'">'
  3109. followStr+=' <img loading="lazy" src="'+followProfileUrl+'"/></a>'
  3110. followStr+=' <p class="followText">'+translate['Stop following']+' '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
  3111. followStr+=' <form method="POST" action="'+originPathStr+'/unfollowconfirm">'
  3112. followStr+=' <input type="hidden" name="actor" value="'+followActor+'">'
  3113. followStr+=' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>'
  3114. followStr+=' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>'
  3115. followStr+=' </form>'
  3116. followStr+='</center>'
  3117. followStr+='</div>'
  3118. followStr+='</div>'
  3119. followStr+=htmlFooter()
  3120. return followStr
  3121. def htmlPersonOptions(translate: {},baseDir: str, \
  3122. domain: str,originPathStr: str, \
  3123. optionsActor: str, \
  3124. optionsProfileUrl: str, \
  3125. optionsLink: str, \
  3126. pageNumber: int, \
  3127. donateUrl: str, \
  3128. xmppAddress: str, \
  3129. matrixAddress: str, \
  3130. PGPpubKey: str, \
  3131. emailAddress) -> str:
  3132. """Show options for a person: view/follow/block/report
  3133. """
  3134. optionsDomain,optionsPort=getDomainFromActor(optionsActor)
  3135. if os.path.isfile(baseDir+'/img/options-background.png'):
  3136. if not os.path.isfile(baseDir+'/accounts/options-background.png'):
  3137. copyfile(baseDir+'/img/options-background.png',baseDir+'/accounts/options-background.png')
  3138. followStr='Follow'
  3139. blockStr='Block'
  3140. nickname=None
  3141. if originPathStr.startswith('/users/'):
  3142. nickname=originPathStr.split('/users/')[1]
  3143. if '/' in nickname:
  3144. nickname=nickname.split('/')[0]
  3145. if '?' in nickname:
  3146. nickname=nickname.split('?')[0]
  3147. followerDomain,followerPort=getDomainFromActor(optionsActor)
  3148. if isFollowingActor(baseDir,nickname,domain,optionsActor):
  3149. followStr='Unfollow'
  3150. optionsNickname=getNicknameFromActor(optionsActor)
  3151. optionsDomainFull=optionsDomain
  3152. if optionsPort:
  3153. if optionsPort!=80 and optionsPort!=443:
  3154. optionsDomainFull=optionsDomain+':'+str(optionsPort)
  3155. if isBlocked(baseDir,nickname,domain,optionsNickname,optionsDomainFull):
  3156. blockStr='Block'
  3157. optionsLinkStr=''
  3158. if optionsLink:
  3159. optionsLinkStr=' <input type="hidden" name="postUrl" value="'+optionsLink+'">'
  3160. cssFilename=baseDir+'/epicyon-follow.css'
  3161. if os.path.isfile(baseDir+'/follow.css'):
  3162. cssFilename=baseDir+'/follow.css'
  3163. with open(cssFilename, 'r') as cssFile:
  3164. profileStyle = cssFile.read()
  3165. # To snooze, or not to snooze? That is the question
  3166. snoozeButtonStr='Snooze'
  3167. if nickname:
  3168. if isPersonSnoozed(baseDir,nickname,domain,optionsActor):
  3169. snoozeButtonStr='Unsnooze'
  3170. donateStr=''
  3171. if donateUrl:
  3172. donateStr= \
  3173. ' <a href="'+donateUrl+'"><button class="button" name="submitDonate">'+translate['Donate']+'</button></a>'
  3174. optionsStr=htmlHeader(cssFilename,profileStyle)
  3175. optionsStr+='<div class="options">'
  3176. optionsStr+=' <div class="optionsAvatar">'
  3177. optionsStr+=' <center>'
  3178. optionsStr+=' <a href="'+optionsActor+'">'
  3179. optionsStr+=' <img loading="lazy" src="'+optionsProfileUrl+'"/></a>'
  3180. optionsStr+=' <p class="optionsText">'+translate['Options for']+' @'+getNicknameFromActor(optionsActor)+'@'+optionsDomain+'</p>'
  3181. if emailAddress:
  3182. optionsStr+='<p class="imText">'+translate['Email']+': <a href="mailto:'+emailAddress+'">'+emailAddress+'</a></p>'
  3183. if xmppAddress:
  3184. optionsStr+='<p class="imText">'+translate['XMPP']+': <a href="xmpp:'+xmppAddress+'">'+xmppAddress+'</a></p>'
  3185. if matrixAddress:
  3186. optionsStr+='<p class="imText">'+translate['Matrix']+': '+matrixAddress+'</p>'
  3187. if PGPpubKey:
  3188. optionsStr+='<p class="pgp">'+PGPpubKey.replace('\n','<br>')+'</p>'
  3189. optionsStr+=' <form method="POST" action="'+originPathStr+'/personoptions">'
  3190. optionsStr+=' <input type="hidden" name="pageNumber" value="'+str(pageNumber)+'">'
  3191. optionsStr+=' <input type="hidden" name="actor" value="'+optionsActor+'">'
  3192. optionsStr+=' <input type="hidden" name="avatarUrl" value="'+optionsProfileUrl+'">'
  3193. optionsStr+=optionsLinkStr
  3194. optionsStr+=' <button type="submit" class="button" name="submitView">'+translate['View']+'</button>'
  3195. optionsStr+=donateStr
  3196. optionsStr+=' <button type="submit" class="button" name="submit'+followStr+'">'+translate[followStr]+'</button>'
  3197. optionsStr+=' <button type="submit" class="button" name="submit'+blockStr+'">'+translate[blockStr]+'</button>'
  3198. optionsStr+=' <button type="submit" class="button" name="submitDM">'+translate['DM']+'</button>'
  3199. optionsStr+=' <button type="submit" class="button" name="submit'+snoozeButtonStr+'">'+translate[snoozeButtonStr]+'</button>'
  3200. optionsStr+=' <button type="submit" class="button" name="submitReport">'+translate['Report']+'</button>'
  3201. optionsStr+=' </form>'
  3202. optionsStr+='</center>'
  3203. optionsStr+='</div>'
  3204. optionsStr+='</div>'
  3205. optionsStr+=htmlFooter()
  3206. return optionsStr
  3207. #def htmlBlockConfirm(translate: {},baseDir: str, \
  3208. # originPathStr: str, \
  3209. # blockActor: str, \
  3210. # blockProfileUrl: str) -> str:
  3211. # """Asks to confirm a block
  3212. # """
  3213. # blockDomain,port=getDomainFromActor(blockActor)
  3214. #
  3215. # if os.path.isfile(baseDir+'/img/block-background.png'):
  3216. # if not os.path.isfile(baseDir+'/accounts/block-background.png'):
  3217. # copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
  3218. #
  3219. # with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  3220. # profileStyle = cssFile.read()
  3221. # blockStr=htmlHeader(cssFilename,profileStyle)
  3222. # blockStr+='<div class="block">'
  3223. # blockStr+=' <div class="blockAvatar">'
  3224. # blockStr+=' <center>'
  3225. # blockStr+=' <a href="'+blockActor+'">'
  3226. # blockStr+=' <img loading="lazy" src="'+blockProfileUrl+'"/></a>'
  3227. # blockStr+=' <p class="blockText">'+translate['Block']+' '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
  3228. # blockStr+=' <form method="POST" action="'+originPathStr+'/blockconfirm">'
  3229. # blockStr+=' <input type="hidden" name="actor" value="'+blockActor+'">'
  3230. # blockStr+=' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>'
  3231. # blockStr+=' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>'
  3232. # blockStr+=' </form>'
  3233. # blockStr+='</center>'
  3234. # blockStr+='</div>'
  3235. # blockStr+='</div>'
  3236. # blockStr+=htmlFooter()
  3237. # return blockStr
  3238. def htmlUnblockConfirm(translate: {},baseDir: str, \
  3239. originPathStr: str, \
  3240. blockActor: str, \
  3241. blockProfileUrl: str) -> str:
  3242. """Asks to confirm unblocking an actor
  3243. """
  3244. blockDomain,port=getDomainFromActor(blockActor)
  3245. if os.path.isfile(baseDir+'/img/block-background.png'):
  3246. if not os.path.isfile(baseDir+'/accounts/block-background.png'):
  3247. copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
  3248. cssFilename=baseDir+'/epicyon-follow.css'
  3249. if os.path.isfile(baseDir+'/follow.css'):
  3250. cssFilename=baseDir+'/follow.css'
  3251. with open(cssFilename, 'r') as cssFile:
  3252. profileStyle = cssFile.read()
  3253. blockStr=htmlHeader(cssFilename,profileStyle)
  3254. blockStr+='<div class="block">'
  3255. blockStr+=' <div class="blockAvatar">'
  3256. blockStr+=' <center>'
  3257. blockStr+=' <a href="'+blockActor+'">'
  3258. blockStr+=' <img loading="lazy" src="'+blockProfileUrl+'"/></a>'
  3259. blockStr+=' <p class="blockText">'+translate['Stop blocking']+' '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
  3260. blockStr+=' <form method="POST" action="'+originPathStr+'/unblockconfirm">'
  3261. blockStr+=' <input type="hidden" name="actor" value="'+blockActor+'">'
  3262. blockStr+=' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>'
  3263. blockStr+=' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>'
  3264. blockStr+=' </form>'
  3265. blockStr+='</center>'
  3266. blockStr+='</div>'
  3267. blockStr+='</div>'
  3268. blockStr+=htmlFooter()
  3269. return blockStr
  3270. def htmlSearchEmojiTextEntry(translate: {}, \
  3271. baseDir: str,path: str) -> str:
  3272. """Search for an emoji by name
  3273. """
  3274. # emoji.json is generated so that it can be customized and the changes
  3275. # will be retained even if default_emoji.json is subsequently updated
  3276. if not os.path.isfile(baseDir+'/emoji/emoji.json'):
  3277. copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
  3278. actor=path.replace('/search','')
  3279. nickname=getNicknameFromActor(actor)
  3280. domain,port=getDomainFromActor(actor)
  3281. if os.path.isfile(baseDir+'/img/search-background.png'):
  3282. if not os.path.isfile(baseDir+'/accounts/search-background.png'):
  3283. copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
  3284. cssFilename=baseDir+'/epicyon-follow.css'
  3285. if os.path.isfile(baseDir+'/follow.css'):
  3286. cssFilename=baseDir+'/follow.css'
  3287. with open(cssFilename, 'r') as cssFile:
  3288. profileStyle = cssFile.read()
  3289. emojiStr=htmlHeader(cssFilename,profileStyle)
  3290. emojiStr+='<div class="follow">'
  3291. emojiStr+=' <div class="followAvatar">'
  3292. emojiStr+=' <center>'
  3293. emojiStr+=' <p class="followText">'+translate['Enter an emoji name to search for']+'</p>'
  3294. emojiStr+=' <form method="POST" action="'+actor+'/searchhandleemoji">'
  3295. emojiStr+=' <input type="hidden" name="actor" value="'+actor+'">'
  3296. emojiStr+=' <input type="text" name="searchtext" autofocus><br>'
  3297. emojiStr+=' <button type="submit" class="button" name="submitSearch">'+translate['Submit']+'</button>'
  3298. emojiStr+=' </form>'
  3299. emojiStr+=' </center>'
  3300. emojiStr+=' </div>'
  3301. emojiStr+='</div>'
  3302. emojiStr+=htmlFooter()
  3303. return emojiStr
  3304. def weekDayOfMonthStart(monthNumber: int,year: int) -> int:
  3305. """Gets the day number of the first day of the month
  3306. 1=sun, 7=sat
  3307. """
  3308. firstDayOfMonth=datetime(year, monthNumber, 1, 0, 0)
  3309. return int(firstDayOfMonth.strftime("%w"))+1
  3310. def getCalendarEvents(baseDir: str,nickname: str,domain: str,year: int,monthNumber: int) -> {}:
  3311. """Retrieves calendar events
  3312. Returns a dictionary indexed by day number of lists containing Event and Place activities
  3313. """
  3314. calendarFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/calendar/'+str(year)+'/'+str(monthNumber)+'.txt'
  3315. events={}
  3316. if not os.path.isfile(calendarFilename):
  3317. return events
  3318. calendarPostIds=[]
  3319. recreateEventsFile=False
  3320. with open(calendarFilename,'r') as eventsFile:
  3321. for postId in eventsFile:
  3322. postId=postId.replace('\n','')
  3323. postFilename=locatePost(baseDir,nickname,domain,postId)
  3324. if postFilename:
  3325. postJsonObject=loadJson(postFilename)
  3326. if postJsonObject:
  3327. if postJsonObject.get('object'):
  3328. if isinstance(postJsonObject['object'], dict):
  3329. if postJsonObject['object'].get('tag'):
  3330. postEvent=[]
  3331. dayOfMonth=None
  3332. for tag in postJsonObject['object']['tag']:
  3333. if not tag.get('type'):
  3334. continue
  3335. if tag['type']!='Event' and tag['type']!='Place':
  3336. continue
  3337. if tag['type']=='Event':
  3338. # tag is an event
  3339. if not tag.get('startTime'):
  3340. continue
  3341. eventTime= \
  3342. datetime.strptime(tag['startTime'], \
  3343. "%Y-%m-%dT%H:%M:%S%z")
  3344. if int(eventTime.strftime("%Y"))==year and \
  3345. int(eventTime.strftime("%m"))==monthNumber:
  3346. dayOfMonth=str(int(eventTime.strftime("%d")))
  3347. postEvent.append(tag)
  3348. else:
  3349. # tag is a place
  3350. postEvent.append(tag)
  3351. if postEvent and dayOfMonth:
  3352. calendarPostIds.append(postId)
  3353. if not events.get(dayOfMonth):
  3354. events[dayOfMonth]=[]
  3355. events[dayOfMonth].append(postEvent)
  3356. else:
  3357. recreateEventsFile=True
  3358. # if some posts have been deleted then regenerate the calendar file
  3359. if recreateEventsFile:
  3360. calendarFile=open(calendarFilename, "w")
  3361. for postId in calendarPostIds:
  3362. calendarFile.write(postId+'\n')
  3363. calendarFile.close()
  3364. return events
  3365. def htmlCalendarDay(translate: {}, \
  3366. baseDir: str,path: str, \
  3367. year: int,monthNumber: int,dayNumber: int,
  3368. nickname: str,domain: str,dayEvents: [], \
  3369. monthName: str, actor: str) -> str:
  3370. """Show a day within the calendar
  3371. """
  3372. accountDir=baseDir+'/accounts/'+nickname+'@'+domain
  3373. calendarFile=accountDir+'/.newCalendar'
  3374. if os.path.isfile(calendarFile):
  3375. os.remove(calendarFile)
  3376. cssFilename=baseDir+'/epicyon-calendar.css'
  3377. if os.path.isfile(baseDir+'/calendar.css'):
  3378. cssFilename=baseDir+'/calendar.css'
  3379. with open(cssFilename, 'r') as cssFile:
  3380. calendarStyle = cssFile.read()
  3381. calendarStr=htmlHeader(cssFilename,calendarStyle)
  3382. calendarStr+='<main><table class="calendar">\n'
  3383. calendarStr+='<caption class="calendar__banner--month">\n'
  3384. calendarStr+=' <a href="'+actor+'/calendar?year='+str(year)+'?month='+str(monthNumber)+'">'
  3385. calendarStr+=' <h1>'+str(dayNumber)+' '+monthName+'</h1></a><br><span class="year">'+str(year)+'</span>\n'
  3386. calendarStr+='</caption>\n'
  3387. calendarStr+='<tbody>\n'
  3388. for eventPost in dayEvents:
  3389. eventTime=None
  3390. eventDescription=None
  3391. eventPlace=None
  3392. for ev in eventPost:
  3393. if ev['type']=='Event':
  3394. if ev.get('startTime'):
  3395. eventDate=datetime.strptime(ev['startTime'],"%Y-%m-%dT%H:%M:%S%z")
  3396. eventTime=eventDate.strftime("%H:%M").strip()
  3397. if ev.get('name'):
  3398. eventDescription=ev['name'].strip()
  3399. elif ev['type']=='Place':
  3400. if ev.get('name'):
  3401. eventPlace=ev['name']
  3402. if eventTime and eventDescription and eventPlace:
  3403. calendarStr+='<tr><td class="calendar__day__time"><b>'+eventTime+'</b></td><td class="calendar__day__event"><span class="place">'+eventPlace+'</span><br>'+eventDescription+'</td></tr>\n'
  3404. elif eventTime and eventDescription and not eventPlace:
  3405. calendarStr+='<tr><td class="calendar__day__time"><b>'+eventTime+'</b></td><td class="calendar__day__event">'+eventDescription+'</td></tr>\n'
  3406. elif not eventTime and eventDescription and not eventPlace:
  3407. calendarStr+='<tr><td class="calendar__day__time"></td><td class="calendar__day__event">'+eventDescription+'</td></tr>\n'
  3408. elif not eventTime and eventDescription and eventPlace:
  3409. calendarStr+='<tr><td class="calendar__day__time"></td><td class="calendar__day__event"><span class="place">'+eventPlace+'</span><br>'+eventDescription+'</td></tr>\n'
  3410. elif eventTime and not eventDescription and eventPlace:
  3411. calendarStr+='<tr><td class="calendar__day__time"><b>'+eventTime+'</b></td><td class="calendar__day__event"><span class="place">'+eventPlace+'</span></td></tr>\n'
  3412. calendarStr+='</tbody>\n'
  3413. calendarStr+='</table></main>\n'
  3414. calendarStr+=htmlFooter()
  3415. return calendarStr
  3416. def htmlCalendar(translate: {}, \
  3417. baseDir: str,path: str, \
  3418. httpPrefix: str,domainFull: str) -> str:
  3419. """Show the calendar for a person
  3420. """
  3421. iconsDir=getIconsDir(baseDir)
  3422. domain=domainFull
  3423. if ':' in domainFull:
  3424. domain=domainFull.split(':')[0]
  3425. monthNumber=0
  3426. dayNumber=None
  3427. year=1970
  3428. actor=httpPrefix+'://'+domainFull+path.replace('/calendar','')
  3429. if '?' in actor:
  3430. first=True
  3431. for p in actor.split('?'):
  3432. if not first:
  3433. if '=' in p:
  3434. if p.split('=')[0]=='year':
  3435. numStr=p.split('=')[1]
  3436. if numStr.isdigit():
  3437. year=int(numStr)
  3438. elif p.split('=')[0]=='month':
  3439. numStr=p.split('=')[1]
  3440. if numStr.isdigit():
  3441. monthNumber=int(numStr)
  3442. elif p.split('=')[0]=='day':
  3443. numStr=p.split('=')[1]
  3444. if numStr.isdigit():
  3445. dayNumber=int(numStr)
  3446. first=False
  3447. actor=actor.split('?')[0]
  3448. currDate=datetime.now()
  3449. if year==1970 and monthNumber==0:
  3450. year=currDate.year
  3451. monthNumber=currDate.month
  3452. nickname=getNicknameFromActor(actor)
  3453. events=getCalendarEvents(baseDir,nickname,domain,year,monthNumber)
  3454. months=('January','February','March','April','May','June','July','August','September','October','November','December')
  3455. monthName=translate[months[monthNumber-1]]
  3456. if os.path.isfile(baseDir+'/img/calendar-background.png'):
  3457. if not os.path.isfile(baseDir+'/accounts/calendar-background.png'):
  3458. copyfile(baseDir+'/img/calendar-background.png',baseDir+'/accounts/calendar-background.png')
  3459. if dayNumber:
  3460. dayEvents=None
  3461. if events.get(str(dayNumber)):
  3462. dayEvents=events[str(dayNumber)]
  3463. return htmlCalendarDay(translate,baseDir,path, \
  3464. year,monthNumber,dayNumber, \
  3465. nickname,domain,dayEvents, \
  3466. monthName,actor)
  3467. prevYear=year
  3468. prevMonthNumber=monthNumber-1
  3469. if prevMonthNumber<1:
  3470. prevMonthNumber=12
  3471. prevYear=year-1
  3472. nextYear=year
  3473. nextMonthNumber=monthNumber+1
  3474. if nextMonthNumber>12:
  3475. nextMonthNumber=1
  3476. nextYear=year+1
  3477. print('Calendar year='+str(year)+' month='+str(monthNumber)+ ' '+str(weekDayOfMonthStart(monthNumber,year)))
  3478. if monthNumber<12:
  3479. daysInMonth=(date(year, monthNumber+1, 1) - date(year, monthNumber, 1)).days
  3480. else:
  3481. daysInMonth=(date(year+1, 1, 1) - date(year, monthNumber, 1)).days
  3482. cssFilename=baseDir+'/epicyon-calendar.css'
  3483. if os.path.isfile(baseDir+'/calendar.css'):
  3484. cssFilename=baseDir+'/calendar.css'
  3485. with open(cssFilename, 'r') as cssFile:
  3486. calendarStyle = cssFile.read()
  3487. calendarStr=htmlHeader(cssFilename,calendarStyle)
  3488. calendarStr+='<main><table class="calendar">\n'
  3489. calendarStr+='<caption class="calendar__banner--month">\n'
  3490. calendarStr+=' <a href="'+actor+'/calendar?year='+str(prevYear)+'?month='+str(prevMonthNumber)+'">'
  3491. calendarStr+=' <img loading="lazy" alt="'+translate['Previous month']+'" title="'+translate['Previous month']+'" src="/'+iconsDir+'/prev.png" class="buttonprev"/></a>\n'
  3492. calendarStr+=' <a href="'+actor+'/inbox">'
  3493. calendarStr+=' <h1>'+monthName+'</h1></a>\n'
  3494. calendarStr+=' <a href="'+actor+'/calendar?year='+str(nextYear)+'?month='+str(nextMonthNumber)+'">'
  3495. calendarStr+=' <img loading="lazy" alt="'+translate['Next month']+'" title="'+translate['Next month']+'" src="/'+iconsDir+'/prev.png" class="buttonnext"/></a>\n'
  3496. calendarStr+='</caption>\n'
  3497. calendarStr+='<thead>\n'
  3498. calendarStr+='<tr>\n'
  3499. calendarStr+=' <th class="calendar__day__header">'+translate['Sun']+'</th>\n'
  3500. calendarStr+=' <th class="calendar__day__header">'+translate['Mon']+'</th>\n'
  3501. calendarStr+=' <th class="calendar__day__header">'+translate['Tue']+'</th>\n'
  3502. calendarStr+=' <th class="calendar__day__header">'+translate['Wed']+'</th>\n'
  3503. calendarStr+=' <th class="calendar__day__header">'+translate['Thu']+'</th>\n'
  3504. calendarStr+=' <th class="calendar__day__header">'+translate['Fri']+'</th>\n'
  3505. calendarStr+=' <th class="calendar__day__header">'+translate['Sat']+'</th>\n'
  3506. calendarStr+='</tr>\n'
  3507. calendarStr+='</thead>\n'
  3508. calendarStr+='<tbody>\n'
  3509. dayOfMonth=0
  3510. dow=weekDayOfMonthStart(monthNumber,year)
  3511. for weekOfMonth in range(1,6):
  3512. calendarStr+=' <tr>\n'
  3513. for dayNumber in range(1,8):
  3514. if (weekOfMonth>1 and dayOfMonth<daysInMonth) or \
  3515. (weekOfMonth==1 and dayNumber>=dow):
  3516. dayOfMonth+=1
  3517. isToday=False
  3518. if year==currDate.year:
  3519. if currDate.month==monthNumber:
  3520. if dayOfMonth==currDate.day:
  3521. isToday=True
  3522. if events.get(str(dayOfMonth)):
  3523. url=actor+'/calendar?year='+str(year)+'?month='+str(monthNumber)+'?day='+str(dayOfMonth)
  3524. dayLink='<a href="'+url+'">'+str(dayOfMonth)+'</a>'
  3525. # there are events for this day
  3526. if not isToday:
  3527. calendarStr+=' <td class="calendar__day__cell" data-event="">'+dayLink+'</td>\n'
  3528. else:
  3529. calendarStr+=' <td class="calendar__day__cell" data-today-event="">'+dayLink+'</td>\n'
  3530. else:
  3531. # No events today
  3532. if not isToday:
  3533. calendarStr+=' <td class="calendar__day__cell">'+str(dayOfMonth)+'</td>\n'
  3534. else:
  3535. calendarStr+=' <td class="calendar__day__cell" data-today="">'+str(dayOfMonth)+'</td>\n'
  3536. else:
  3537. calendarStr+=' <td class="calendar__day__cell"></td>\n'
  3538. calendarStr+=' </tr>\n'
  3539. calendarStr+='</tbody>\n'
  3540. calendarStr+='</table></main>\n'
  3541. calendarStr+=htmlFooter()
  3542. return calendarStr
  3543. def htmlHashTagSwarm(baseDir: str,actor: str) -> str:
  3544. """Returns a tag swarm of today's hashtags
  3545. """
  3546. daysSinceEpoch=(datetime.utcnow() - datetime(1970,1,1)).days
  3547. daysSinceEpochStr=str(daysSinceEpoch)+' '
  3548. nickname=getNicknameFromActor(actor)
  3549. tagSwarm=[]
  3550. for subdir, dirs, files in os.walk(baseDir+'/tags'):
  3551. for f in files:
  3552. tagsFilename=os.path.join(baseDir+'/tags',f)
  3553. if not os.path.isfile(tagsFilename):
  3554. continue
  3555. hashTagName=f.split('.')[0]
  3556. if isBlockedHashtag(baseDir,hashTagName):
  3557. continue
  3558. if daysSinceEpochStr not in open(tagsFilename).read():
  3559. continue
  3560. with open(tagsFilename, 'r') as tagsFile:
  3561. lines=tagsFile.readlines()
  3562. for l in lines:
  3563. if ' ' not in l:
  3564. continue
  3565. postDaysSinceEpochStr=l.split(' ')[0]
  3566. if not postDaysSinceEpochStr.isdigit():
  3567. continue
  3568. postDaysSinceEpoch=int(postDaysSinceEpochStr)
  3569. if postDaysSinceEpoch<daysSinceEpoch:
  3570. break
  3571. if postDaysSinceEpoch==daysSinceEpoch:
  3572. tagSwarm.append(hashTagName)
  3573. break
  3574. if not tagSwarm:
  3575. return ''
  3576. tagSwarm.sort()
  3577. tagSwarmStr=''
  3578. for tagName in tagSwarm:
  3579. tagSwarmStr+='<a href="'+actor+'/tags/'+tagName+'" class="hashtagswarm">'+tagName+'</a> '
  3580. tagSwarmHtml=tagSwarmStr.strip()+'\n'
  3581. return tagSwarmHtml
  3582. def htmlSearch(translate: {}, \
  3583. baseDir: str,path: str) -> str:
  3584. """Search called from the timeline icon
  3585. """
  3586. actor=path.replace('/search','')
  3587. nickname=getNicknameFromActor(actor)
  3588. domain,port=getDomainFromActor(actor)
  3589. if os.path.isfile(baseDir+'/img/search-background.png'):
  3590. if not os.path.isfile(baseDir+'/accounts/search-background.png'):
  3591. copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
  3592. cssFilename=baseDir+'/epicyon-follow.css'
  3593. if os.path.isfile(baseDir+'/follow.css'):
  3594. cssFilename=baseDir+'/follow.css'
  3595. with open(cssFilename, 'r') as cssFile:
  3596. profileStyle = cssFile.read()
  3597. followStr=htmlHeader(cssFilename,profileStyle)
  3598. followStr+='<div class="follow">'
  3599. followStr+=' <div class="followAvatar">'
  3600. followStr+=' <center>'
  3601. followStr+=' <p class="followText">'+translate['Enter an address, shared item, #hashtag, *skill or :emoji: to search for']+'</p>'
  3602. followStr+=' <form method="POST" accept-charset="UTF-8" action="'+actor+'/searchhandle">'
  3603. followStr+=' <input type="hidden" name="actor" value="'+actor+'">'
  3604. followStr+=' <input type="text" name="searchtext" autofocus><br>'
  3605. followStr+=' <button type="submit" class="button" name="submitSearch">'+translate['Submit']+'</button>'
  3606. followStr+=' <button type="submit" class="button" name="submitBack">'+translate['Go Back']+'</button>'
  3607. followStr+=' </form>'
  3608. followStr+=' <p class="hashtagswarm">'+htmlHashTagSwarm(baseDir,actor)+'</p>'
  3609. followStr+=' </center>'
  3610. followStr+=' </div>'
  3611. followStr+='</div>'
  3612. followStr+=htmlFooter()
  3613. return followStr
  3614. def htmlProfileAfterSearch(recentPostsCache: {},maxRecentPosts: int, \
  3615. translate: {}, \
  3616. baseDir: str,path: str,httpPrefix: str, \
  3617. nickname: str,domain: str,port: int, \
  3618. profileHandle: str, \
  3619. session,wfRequest: {},personCache: {},
  3620. debug: bool,projectVersion: str) -> str:
  3621. """Show a profile page after a search for a fediverse address
  3622. """
  3623. if '/users/' in profileHandle or \
  3624. '/channel/' in profileHandle or \
  3625. '/profile/' in profileHandle or \
  3626. '/@' in profileHandle:
  3627. searchNickname=getNicknameFromActor(profileHandle)
  3628. searchDomain,searchPort=getDomainFromActor(profileHandle)
  3629. else:
  3630. if '@' not in profileHandle:
  3631. if debug:
  3632. print('DEBUG: no @ in '+profileHandle)
  3633. return None
  3634. if profileHandle.startswith('@'):
  3635. profileHandle=profileHandle[1:]
  3636. if '@' not in profileHandle:
  3637. if debug:
  3638. print('DEBUG: no @ in '+profileHandle)
  3639. return None
  3640. searchNickname=profileHandle.split('@')[0]
  3641. searchDomain=profileHandle.split('@')[1]
  3642. searchPort=None
  3643. if ':' in searchDomain:
  3644. searchPort=int(searchDomain.split(':')[1])
  3645. searchDomain=searchDomain.split(':')[0]
  3646. if not searchNickname:
  3647. if debug:
  3648. print('DEBUG: No nickname found in '+profileHandle)
  3649. return None
  3650. if not searchDomain:
  3651. if debug:
  3652. print('DEBUG: No domain found in '+profileHandle)
  3653. return None
  3654. searchDomainFull=searchDomain
  3655. if searchPort:
  3656. if searchPort!=80 and searchPort!=443:
  3657. if ':' not in searchDomain:
  3658. searchDomainFull=searchDomain+':'+str(searchPort)
  3659. profileStr=''
  3660. cssFilename=baseDir+'/epicyon-profile.css'
  3661. if os.path.isfile(baseDir+'/epicyon.css'):
  3662. cssFilename=baseDir+'/epicyon.css'
  3663. with open(cssFilename, 'r') as cssFile:
  3664. wf = webfingerHandle(session,searchNickname+'@'+searchDomainFull,httpPrefix,wfRequest, \
  3665. domain,projectVersion)
  3666. if not wf:
  3667. if debug:
  3668. print('DEBUG: Unable to webfinger '+searchNickname+'@'+searchDomainFull)
  3669. return None
  3670. personUrl=None
  3671. if wf.get('errors'):
  3672. personUrl=httpPrefix+'://'+searchDomainFull+'/users/'+searchNickname
  3673. asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
  3674. if not personUrl:
  3675. personUrl = getUserUrl(wf)
  3676. if not personUrl:
  3677. # try single user instance
  3678. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  3679. personUrl=httpPrefix+'://'+searchDomainFull
  3680. profileJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
  3681. if not profileJson:
  3682. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  3683. profileJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
  3684. if not profileJson:
  3685. if debug:
  3686. print('DEBUG: No actor returned from '+personUrl)
  3687. return None
  3688. avatarUrl=''
  3689. if profileJson.get('icon'):
  3690. if profileJson['icon'].get('url'):
  3691. avatarUrl=profileJson['icon']['url']
  3692. if not avatarUrl:
  3693. avatarUrl=getPersonAvatarUrl(baseDir,personUrl,personCache)
  3694. displayName=searchNickname
  3695. if profileJson.get('name'):
  3696. displayName=profileJson['name']
  3697. profileDescription=''
  3698. if profileJson.get('summary'):
  3699. profileDescription=profileJson['summary']
  3700. outboxUrl=None
  3701. if not profileJson.get('outbox'):
  3702. if debug:
  3703. pprint(profileJson)
  3704. print('DEBUG: No outbox found')
  3705. return None
  3706. outboxUrl=profileJson['outbox']
  3707. profileBackgroundImage=''
  3708. if profileJson.get('image'):
  3709. if profileJson['image'].get('url'):
  3710. profileBackgroundImage=profileJson['image']['url']
  3711. profileStyle = cssFile.read().replace('image.png',profileBackgroundImage)
  3712. if httpPrefix!='https':
  3713. profileStyle=profileStyle.replace('https://',httpPrefix+'://')
  3714. # url to return to
  3715. backUrl=path
  3716. if not backUrl.endswith('/inbox'):
  3717. backUrl+='/inbox'
  3718. profileDescriptionShort=profileDescription
  3719. if '\n' in profileDescription:
  3720. if len(profileDescription.split('\n'))>2:
  3721. profileDescriptionShort=''
  3722. else:
  3723. if '<br>' in profileDescription:
  3724. if len(profileDescription.split('<br>'))>2:
  3725. profileDescriptionShort=''
  3726. # keep the profile description short
  3727. if len(profileDescriptionShort)>256:
  3728. profileDescriptionShort=''
  3729. # remove formatting from profile description used on title
  3730. avatarDescription=''
  3731. if profileJson.get('summary'):
  3732. avatarDescription=profileJson['summary'].replace('<br>','\n').replace('<p>','').replace('</p>','')
  3733. profileStr=' <div class="hero-image">'
  3734. profileStr+=' <div class="hero-text">'
  3735. profileStr+=' <img loading="lazy" src="'+avatarUrl+'" alt="'+avatarDescription+'" title="'+avatarDescription+'">'
  3736. profileStr+=' <h1>'+displayName+'</h1>'
  3737. profileStr+=' <p><b>@'+searchNickname+'@'+searchDomainFull+'</b></p>'
  3738. profileStr+=' <p>'+profileDescriptionShort+'</p>'
  3739. profileStr+=' </div>'
  3740. profileStr+='</div>'
  3741. profileStr+='<div class="container">\n'
  3742. profileStr+=' <form method="POST" action="'+backUrl+'/followconfirm">'
  3743. profileStr+=' <center>'
  3744. profileStr+=' <input type="hidden" name="actor" value="'+personUrl+'">'
  3745. profileStr+=' <button type="submit" class="button" name="submitYes">'+translate['Follow']+'</button>'
  3746. profileStr+=' <button type="submit" class="button" name="submitView">'+translate['View']+'</button>'
  3747. profileStr+=' <a href="'+backUrl+'"><button class="button">'+translate['Go Back']+'</button></a>'
  3748. profileStr+=' </center>'
  3749. profileStr+=' </form>'
  3750. profileStr+='</div>'
  3751. profileStr+='<script>'+contentWarningScript()+'</script>'
  3752. iconsDir=getIconsDir(baseDir)
  3753. result = []
  3754. i = 0
  3755. for item in parseUserFeed(session,outboxUrl,asHeader, \
  3756. projectVersion,httpPrefix,domain):
  3757. if not item.get('type'):
  3758. continue
  3759. if item['type']!='Create' and item['type']!='Announce':
  3760. continue
  3761. if not item.get('object'):
  3762. continue
  3763. profileStr+= \
  3764. individualPostAsHtml(recentPostsCache,maxRecentPosts, \
  3765. iconsDir,translate,None,baseDir, \
  3766. session,wfRequest,personCache, \
  3767. nickname,domain,port, \
  3768. item,avatarUrl,False,False, \
  3769. httpPrefix,projectVersion,'inbox', \
  3770. False,False,False,False,False)
  3771. i+=1
  3772. if i>=20:
  3773. break
  3774. return htmlHeader(cssFilename,profileStyle)+profileStr+htmlFooter()