webinterface.py 151 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254
  1. __filename__ = "webinterface.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.0.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import json
  9. import time
  10. import os
  11. import commentjson
  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 utils import getNicknameFromActor
  20. from utils import getDomainFromActor
  21. from utils import locatePost
  22. from utils import noOfAccounts
  23. from utils import isPublicPost
  24. from utils import getDisplayName
  25. from follow import isFollowingActor
  26. from webfinger import webfingerHandle
  27. from posts import isDM
  28. from posts import getPersonBox
  29. from posts import getUserUrl
  30. from posts import parseUserFeed
  31. from posts import populateRepliesJson
  32. from posts import isModerator
  33. from posts import outboxMessageCreateWrap
  34. from posts import downloadAnnounce
  35. from session import getJson
  36. from auth import createPassword
  37. from like import likedByPerson
  38. from like import noOfLikes
  39. from announce import announcedByPerson
  40. from blocking import isBlocked
  41. from content import getMentionsFromHtml
  42. from content import addHtmlTags
  43. from content import replaceEmojiFromTags
  44. from config import getConfigParam
  45. from skills import getSkills
  46. from cache import getPersonFromCache
  47. from cache import storePersonInCache
  48. def updateAvatarImageCache(session,baseDir: str,httpPrefix: str,actor: str,avatarUrl: str,personCache: {},force=False) -> str:
  49. """Updates the cached avatar for the given actor
  50. """
  51. if not avatarUrl:
  52. return None
  53. if avatarUrl.endswith('.png') or '.png?' in avatarUrl:
  54. sessionHeaders = {'Accept': 'image/png'}
  55. avatarImageFilename=baseDir+'/cache/avatars/'+actor.replace('/','-')+'.png'
  56. elif avatarUrl.endswith('.jpg') or avatarUrl.endswith('.jpeg') or \
  57. '.jpg?' in avatarUrl or '.jpeg?' in avatarUrl:
  58. sessionHeaders = {'Accept': 'image/jpeg'}
  59. avatarImageFilename=baseDir+'/cache/avatars/'+actor.replace('/','-')+'.jpg'
  60. elif avatarUrl.endswith('.gif') or '.gif?' in avatarUrl:
  61. sessionHeaders = {'Accept': 'image/gif'}
  62. avatarImageFilename=baseDir+'/cache/avatars/'+actor.replace('/','-')+'.gif'
  63. else:
  64. return None
  65. if not os.path.isfile(avatarImageFilename) or force:
  66. try:
  67. print('avatar image url: '+avatarUrl)
  68. result=session.get(avatarUrl, headers=sessionHeaders, params=None)
  69. if result.status_code<200 or result.status_code>202:
  70. print('Avatar image download failed with status '+str(result.status_code))
  71. # remove partial download
  72. if os.path.isfile(avatarImageFilename):
  73. os.remove(avatarImageFilename)
  74. else:
  75. with open(avatarImageFilename, 'wb') as f:
  76. f.write(result.content)
  77. print('avatar image downloaded for '+actor)
  78. return avatarImageFilename.replace(baseDir+'/cache','')
  79. except Exception as e:
  80. print('Failed to download avatar image: '+str(avatarUrl))
  81. print(e)
  82. sessionHeaders = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
  83. personJson = getJson(session,actor,sessionHeaders,None,__version__,httpPrefix,None)
  84. if personJson:
  85. if not personJson.get('id'):
  86. return None
  87. if not personJson.get('publicKey'):
  88. return None
  89. if not personJson['publicKey'].get('publicKeyPem'):
  90. return None
  91. if personJson['id']!=actor:
  92. return None
  93. if not personCache.get(actor):
  94. return None
  95. if personCache[actor]['actor']['publicKey']['publicKeyPem']!=personJson['publicKey']['publicKeyPem']:
  96. print("ERROR: public keys don't match when downloading actor for "+actor)
  97. return None
  98. storePersonInCache(baseDir,actor,personJson,personCache)
  99. return getPersonAvatarUrl(baseDir,actor,personCache)
  100. return None
  101. return avatarImageFilename.replace(baseDir+'/cache','')
  102. def getPersonAvatarUrl(baseDir: str,personUrl: str,personCache: {}) -> str:
  103. """Returns the avatar url for the person
  104. """
  105. personJson = getPersonFromCache(baseDir,personUrl,personCache)
  106. if personJson:
  107. # get from locally stored image
  108. actorStr=personJson['id'].replace('/','-')
  109. avatarImageFilename=baseDir+'/cache/avatars/'+actorStr+'.png'
  110. if os.path.isfile(avatarImageFilename):
  111. return '/avatars/'+actorStr+'.png'
  112. avatarImageFilename=baseDir+'/cache/avatars/'+actorStr+'.jpg'
  113. if os.path.isfile(avatarImageFilename):
  114. return '/avatars/'+actorStr+'.jpg'
  115. avatarImageFilename=baseDir+'/cache/avatars/'+actorStr+'.gif'
  116. if os.path.isfile(avatarImageFilename):
  117. return '/avatars/'+actorStr+'.gif'
  118. if personJson.get('icon'):
  119. if personJson['icon'].get('url'):
  120. return personJson['icon']['url']
  121. return None
  122. def htmlSearchEmoji(translate: {},baseDir: str,searchStr: str) -> str:
  123. """Search results for emoji
  124. """
  125. if not os.path.isfile(baseDir+'/emoji/emoji.json'):
  126. copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
  127. searchStr=searchStr.lower().replace(':','').strip('\n')
  128. cssFilename=baseDir+'/epicyon-profile.css'
  129. if os.path.isfile(baseDir+'/epicyon.css'):
  130. cssFilename=baseDir+'/epicyon.css'
  131. with open(cssFilename, 'r') as cssFile:
  132. emojiCSS=cssFile.read()
  133. emojiLookupFilename=baseDir+'/emoji/emoji.json'
  134. # create header
  135. emojiForm=htmlHeader(cssFilename,emojiCSS)
  136. emojiForm+='<center><h1>'+translate['Emoji Search']+'</h1></center>'
  137. # does the lookup file exist?
  138. if not os.path.isfile(emojiLookupFilename):
  139. emojiForm+='<center><h5>'+translate['No results']+'</h5></center>'
  140. emojiForm+=htmlFooter()
  141. return emojiForm
  142. loadedEmoji=False
  143. tries=0
  144. while tries<5:
  145. try:
  146. with open(emojiLookupFilename, 'r') as fp:
  147. emojiJson=commentjson.load(fp)
  148. loadedEmoji=True
  149. break
  150. except Exception as e:
  151. print(e)
  152. time.sleep(1)
  153. tries+=1
  154. if loadedEmoji:
  155. results={}
  156. for emojiName,filename in emojiJson.items():
  157. if searchStr in emojiName:
  158. results[emojiName] = filename+'.png'
  159. for emojiName,filename in emojiJson.items():
  160. if emojiName in searchStr:
  161. results[emojiName] = filename+'.png'
  162. headingShown=False
  163. emojiForm+='<center>'
  164. for emojiName,filename in results.items():
  165. if os.path.isfile(baseDir+'/emoji/'+filename):
  166. if not headingShown:
  167. emojiForm+='<center><h5>'+translate['Copy the text then paste it into your post']+'</h5></center>'
  168. headingShown=True
  169. emojiForm+='<h3>:'+emojiName+':<img class="searchEmoji" src="/emoji/'+filename+'"/></h3>'
  170. emojiForm+='</center>'
  171. emojiForm+=htmlFooter()
  172. return emojiForm
  173. def getIconsDir(baseDir: str) -> str:
  174. """Returns the directory where icons exist
  175. """
  176. iconsDir='icons'
  177. theme=getConfigParam(baseDir,'theme')
  178. if theme:
  179. if os.path.isdir(baseDir+'/img/icons/'+theme):
  180. iconsDir='icons/'+theme
  181. return iconsDir
  182. def htmlSearchSharedItems(translate: {}, \
  183. baseDir: str,searchStr: str, \
  184. pageNumber: int, \
  185. resultsPerPage: int, \
  186. httpPrefix: str, \
  187. domainFull: str,actor: str) -> str:
  188. """Search results for shared items
  189. """
  190. iconsDir=getIconsDir(baseDir)
  191. currPage=1
  192. ctr=0
  193. sharedItemsForm=''
  194. searchStrLower=searchStr.replace('%2B','+').replace('%40','@').replace('%3A',':').replace('%23','#').lower().strip('\n')
  195. searchStrLowerList=searchStrLower.split('+')
  196. cssFilename=baseDir+'/epicyon-profile.css'
  197. if os.path.isfile(baseDir+'/epicyon.css'):
  198. cssFilename=baseDir+'/epicyon.css'
  199. with open(cssFilename, 'r') as cssFile:
  200. sharedItemsCSS=cssFile.read()
  201. sharedItemsForm=htmlHeader(cssFilename,sharedItemsCSS)
  202. sharedItemsForm+='<center><h1>'+translate['Shared Items Search']+'</h1></center>'
  203. resultsExist=False
  204. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  205. for handle in dirs:
  206. if '@' not in handle:
  207. continue
  208. contactNickname=handle.split('@')[0]
  209. sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
  210. if not os.path.isfile(sharesFilename):
  211. continue
  212. sharesJson=None
  213. tries=0
  214. while tries<5:
  215. try:
  216. with open(sharesFilename, 'r') as fp:
  217. sharesJson=commentjson.load(fp)
  218. break
  219. except Exception as e:
  220. print(e)
  221. time.sleep(1)
  222. tries+=1
  223. if not sharesJson:
  224. continue
  225. for name,sharedItem in sharesJson.items():
  226. matched=True
  227. for searchSubstr in searchStrLowerList:
  228. subStrMatched=False
  229. searchSubstr=searchSubstr.strip()
  230. if searchSubstr in sharedItem['location'].lower():
  231. subStrMatched=True
  232. elif searchSubstr in sharedItem['summary'].lower():
  233. subStrMatched=True
  234. elif searchSubstr in sharedItem['displayName'].lower():
  235. subStrMatched=True
  236. elif searchSubstr in sharedItem['category'].lower():
  237. subStrMatched=True
  238. if not subStrMatched:
  239. matched=False
  240. break
  241. if matched:
  242. if currPage==pageNumber:
  243. sharedItemsForm+='<div class="container">'
  244. sharedItemsForm+='<p class="share-title">'+sharedItem['displayName']+'</p>'
  245. if sharedItem.get('imageUrl'):
  246. sharedItemsForm+='<a href="'+sharedItem['imageUrl']+'">'
  247. sharedItemsForm+='<img src="'+sharedItem['imageUrl']+'" alt="Item image"></a>'
  248. sharedItemsForm+='<p>'+sharedItem['summary']+'</p>'
  249. sharedItemsForm+='<p><b>'+translate['Type']+':</b> '+sharedItem['itemType']+' '
  250. sharedItemsForm+='<b>'+translate['Category']+':</b> '+sharedItem['category']+' '
  251. sharedItemsForm+='<b>'+translate['Location']+':</b> '+sharedItem['location']+'</p>'
  252. contactActor=httpPrefix+'://'+domainFull+'/users/'+contactNickname
  253. sharedItemsForm+='<p><a href="'+actor+'?replydm=sharedesc:'+sharedItem['displayName']+'?mention='+contactActor+'"><button class="button">'+translate['Contact']+'</button></a>'
  254. if actor.endswith('/users/'+contactNickname):
  255. sharedItemsForm+=' <a href="'+actor+'?rmshare='+name+'"><button class="button">'+translate['Remove']+'</button></a>'
  256. sharedItemsForm+='</p></div>'
  257. if not resultsExist and currPage>1:
  258. # previous page link, needs to be a POST
  259. sharedItemsForm+= \
  260. '<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber-1)+'">' \
  261. ' <input type="hidden" name="actor" value="'+actor+'">' \
  262. ' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>' \
  263. ' <center><a href="'+actor+'" type="submit" name="submitSearch">' \
  264. ' <img class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"/></a>' \
  265. ' </center>' \
  266. '</form>'
  267. resultsExist=True
  268. ctr+=1
  269. if ctr>=resultsPerPage:
  270. currPage+=1
  271. if currPage>pageNumber:
  272. # next page link, needs to be a POST
  273. sharedItemsForm+= \
  274. '<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber+1)+'">' \
  275. ' <input type="hidden" name="actor" value="'+actor+'">' \
  276. ' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>' \
  277. ' <center><a href="'+actor+'" type="submit" name="submitSearch">' \
  278. ' <img class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"/></a>' \
  279. ' </center>' \
  280. '</form>'
  281. break
  282. ctr=0
  283. if not resultsExist:
  284. sharedItemsForm+='<center><h5>'+translate['No results']+'</h5></center>'
  285. sharedItemsForm+=htmlFooter()
  286. return sharedItemsForm
  287. def htmlModerationInfo(translate: {},baseDir: str) -> str:
  288. infoForm=''
  289. cssFilename=baseDir+'/epicyon-profile.css'
  290. if os.path.isfile(baseDir+'/epicyon.css'):
  291. cssFilename=baseDir+'/epicyon.css'
  292. with open(cssFilename, 'r') as cssFile:
  293. infoCSS=cssFile.read()
  294. infoForm=htmlHeader(cssFilename,infoCSS)
  295. infoForm+='<center><h1>'+translate['Moderation Information']+'</h1></center>'
  296. infoShown=False
  297. suspendedFilename=baseDir+'/accounts/suspended.txt'
  298. if os.path.isfile(suspendedFilename):
  299. with open(suspendedFilename, "r") as f:
  300. suspendedStr = f.read()
  301. infoForm+= \
  302. '<div class="container">' \
  303. ' <br><b>'+translate['Suspended accounts']+'</b>' \
  304. ' <br>'+translate['These are currently suspended']+ \
  305. ' <textarea id="message" name="suspended" style="height:200px">'+suspendedStr+'</textarea>' \
  306. '</div>'
  307. infoShown=True
  308. blockingFilename=baseDir+'/accounts/blocking.txt'
  309. if os.path.isfile(blockingFilename):
  310. with open(blockingFilename, "r") as f:
  311. blockedStr = f.read()
  312. infoForm+= \
  313. '<div class="container">' \
  314. ' <br><b>'+translate['Blocked accounts and hashtags']+'</b>' \
  315. ' <br>'+translate['These are globally blocked for all accounts on this instance']+ \
  316. ' <textarea id="message" name="blocked" style="height:200px">'+blockedStr+'</textarea>' \
  317. '</div>'
  318. infoShown=True
  319. if not infoShown:
  320. infoForm+='<center><p>'+translate['Any blocks or suspensions made by moderators will be shown here.']+'</p></center>'
  321. infoForm+=htmlFooter()
  322. return infoForm
  323. def htmlHashtagSearch(translate: {}, \
  324. baseDir: str,hashtag: str,pageNumber: int,postsPerPage: int, \
  325. session,wfRequest: {},personCache: {}, \
  326. httpPrefix: str,projectVersion: str) -> str:
  327. """Show a page containing search results for a hashtag
  328. """
  329. iconsDir=getIconsDir(baseDir)
  330. if hashtag.startswith('#'):
  331. hashtag=hashtag[1:]
  332. hashtagIndexFile=baseDir+'/tags/'+hashtag+'.txt'
  333. if not os.path.isfile(hashtagIndexFile):
  334. return None
  335. # read the index
  336. with open(hashtagIndexFile, "r") as f:
  337. lines = f.readlines()
  338. cssFilename=baseDir+'/epicyon-profile.css'
  339. if os.path.isfile(baseDir+'/epicyon.css'):
  340. cssFilename=baseDir+'/epicyon.css'
  341. with open(cssFilename, 'r') as cssFile:
  342. hashtagSearchCSS = cssFile.read()
  343. startIndex=len(lines)-1-int(pageNumber*postsPerPage)
  344. if startIndex<0:
  345. startIndex=len(lines)-1
  346. endIndex=startIndex-postsPerPage
  347. if endIndex<0:
  348. endIndex=0
  349. hashtagSearchForm=htmlHeader(cssFilename,hashtagSearchCSS)
  350. hashtagSearchForm+='<center><h1>#'+hashtag+'</h1></center>'
  351. if startIndex!=len(lines)-1:
  352. # previous page link
  353. hashtagSearchForm+='<center><a href="/tags/'+hashtag+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"></a></center>'
  354. index=startIndex
  355. while index>=endIndex:
  356. postId=lines[index].strip('\n')
  357. nickname=getNicknameFromActor(postId)
  358. if not nickname:
  359. index-=1
  360. continue
  361. domain,port=getDomainFromActor(postId)
  362. if not domain:
  363. index-=1
  364. continue
  365. postFilename=locatePost(baseDir,nickname,domain,postId)
  366. if not postFilename:
  367. index-=1
  368. continue
  369. loadedPost=False
  370. tries=0
  371. while tries<5:
  372. try:
  373. with open(postFilename, 'r') as fp:
  374. postJsonObject=commentjson.load(fp)
  375. loadedPost=True
  376. break
  377. except Exception as e:
  378. print(e)
  379. time.sleep(1)
  380. tries+=1
  381. if loadedPost:
  382. if not isPublicPost(postJsonObject):
  383. index-=1
  384. continue
  385. hashtagSearchForm+= \
  386. individualPostAsHtml(iconsDir,translate,None, \
  387. baseDir,session,wfRequest,personCache, \
  388. nickname,domain,port,postJsonObject, \
  389. None,True,False, \
  390. httpPrefix,projectVersion,'inbox', \
  391. False,False,False,False)
  392. index-=1
  393. if endIndex>0:
  394. # next page link
  395. hashtagSearchForm+='<center><a href="/tags/'+hashtag+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"></a></center>'
  396. hashtagSearchForm+=htmlFooter()
  397. return hashtagSearchForm
  398. def htmlSkillsSearch(translate: {},baseDir: str, \
  399. skillsearch: str,instanceOnly: bool, \
  400. postsPerPage: int) -> str:
  401. """Show a page containing search results for a skill
  402. """
  403. if skillsearch.startswith('*'):
  404. skillsearch=skillsearch[1:].strip()
  405. skillsearch=skillsearch.lower().strip('\n')
  406. results=[]
  407. # search instance accounts
  408. for subdir, dirs, files in os.walk(baseDir+'/accounts/'):
  409. for f in files:
  410. if not f.endswith('.json'):
  411. continue
  412. if '@' not in f:
  413. continue
  414. if f.startswith('inbox@'):
  415. continue
  416. actorFilename = os.path.join(subdir, f)
  417. loadedActor=False
  418. tries=0
  419. while tries<5:
  420. try:
  421. with open(actorFilename, 'r') as fp:
  422. actorJson=commentjson.load(fp)
  423. loadedActor=True
  424. break
  425. except Exception as e:
  426. print(e)
  427. time.sleep(1)
  428. tries+=1
  429. if loadedActor:
  430. if actorJson.get('id') and \
  431. actorJson.get('skills') and \
  432. actorJson.get('name') and \
  433. actorJson.get('icon'):
  434. actor=actorJson['id']
  435. for skillName,skillLevel in actorJson['skills'].items():
  436. skillName=skillName.lower()
  437. if skillName in skillsearch or skillsearch in skillName:
  438. skillLevelStr=str(skillLevel)
  439. if skillLevel<100:
  440. skillLevelStr='0'+skillLevelStr
  441. if skillLevel<10:
  442. skillLevelStr='0'+skillLevelStr
  443. indexStr=skillLevelStr+';'+actor+';'+actorJson['name']+';'+actorJson['icon']['url']
  444. if indexStr not in results:
  445. results.append(indexStr)
  446. if not instanceOnly:
  447. # search actor cache
  448. for subdir, dirs, files in os.walk(baseDir+'/cache/actors/'):
  449. for f in files:
  450. if not f.endswith('.json'):
  451. continue
  452. if '@' not in f:
  453. continue
  454. if f.startswith('inbox@'):
  455. continue
  456. actorFilename = os.path.join(subdir, f)
  457. loadedActor=False
  458. tries=0
  459. while tries<5:
  460. try:
  461. with open(actorFilename, 'r') as fp:
  462. cachedActorJson=commentjson.load(fp)
  463. loadedActor=True
  464. break
  465. except Exception as e:
  466. print(e)
  467. time.sleep(1)
  468. tries+=1
  469. if loadedActor:
  470. if cachedActorJson.get('actor'):
  471. actorJson=cachedActorJson['actor']
  472. if actorJson.get('id') and \
  473. actorJson.get('skills') and \
  474. actorJson.get('name') and \
  475. actorJson.get('icon'):
  476. actor=actorJson['id']
  477. for skillName,skillLevel in actorJson['skills'].items():
  478. skillName=skillName.lower()
  479. if skillName in skillsearch or skillsearch in skillName:
  480. skillLevelStr=str(skillLevel)
  481. if skillLevel<100:
  482. skillLevelStr='0'+skillLevelStr
  483. if skillLevel<10:
  484. skillLevelStr='0'+skillLevelStr
  485. indexStr=skillLevelStr+';'+actor+';'+actorJson['name']+';'+actorJson['icon']['url']
  486. if indexStr not in results:
  487. results.append(indexStr)
  488. results.sort(reverse=True)
  489. cssFilename=baseDir+'/epicyon-profile.css'
  490. if os.path.isfile(baseDir+'/epicyon.css'):
  491. cssFilename=baseDir+'/epicyon.css'
  492. with open(cssFilename, 'r') as cssFile:
  493. skillSearchCSS = cssFile.read()
  494. skillSearchForm=htmlHeader(cssFilename,skillSearchCSS)
  495. skillSearchForm+='<center><h1>'+translate['Skills search']+': '+skillsearch+'</h1></center>'
  496. if len(results)==0:
  497. skillSearchForm+='<center><h5>'+translate['No results']+'</h5></center>'
  498. else:
  499. skillSearchForm+='<center>'
  500. ctr=0
  501. for skillMatch in results:
  502. skillMatchFields=skillMatch.split(';')
  503. if len(skillMatchFields)==4:
  504. actor=skillMatchFields[1]
  505. actorName=skillMatchFields[2]
  506. avatarUrl=skillMatchFields[3]
  507. skillSearchForm+='<div class="search-result""><a href="'+actor+'/skills">'
  508. skillSearchForm+='<img src="'+avatarUrl+'"/><span class="search-result-text">'+actorName+'</span></a></div>'
  509. ctr+=1
  510. if ctr>=postsPerPage:
  511. break
  512. skillSearchForm+='</center>'
  513. skillSearchForm+=htmlFooter()
  514. return skillSearchForm
  515. def htmlEditProfile(translate: {},baseDir: str,path: str,domain: str,port: int) -> str:
  516. """Shows the edit profile screen
  517. """
  518. pathOriginal=path
  519. path=path.replace('/inbox','').replace('/outbox','').replace('/shares','')
  520. nickname=getNicknameFromActor(path)
  521. if not nickname:
  522. return ''
  523. domainFull=domain
  524. if port:
  525. if port!=80 and port!=443:
  526. if ':' not in domain:
  527. domainFull=domain+':'+str(port)
  528. actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
  529. if not os.path.isfile(actorFilename):
  530. return ''
  531. isBot=''
  532. isGroup=''
  533. displayNickname=nickname
  534. bioStr=''
  535. manuallyApprovesFollowers=''
  536. loadedActor=False
  537. tries=0
  538. while tries<5:
  539. try:
  540. with open(actorFilename, 'r') as fp:
  541. actorJson=commentjson.load(fp)
  542. loadedActor=True
  543. break
  544. except Exception as e:
  545. print(e)
  546. time.sleep(1)
  547. tries+=1
  548. if loadedActor:
  549. if actorJson.get('name'):
  550. displayNickname=actorJson['name']
  551. if actorJson.get('summary'):
  552. bioStr=actorJson['summary'].replace('<p>','').replace('</p>','')
  553. if actorJson.get('manuallyApprovesFollowers'):
  554. if actorJson['manuallyApprovesFollowers']:
  555. manuallyApprovesFollowers='checked'
  556. else:
  557. manuallyApprovesFollowers=''
  558. if actorJson.get('type'):
  559. if actorJson['type']=='Service':
  560. isBot='checked'
  561. isGroup=''
  562. elif actorJson['type']=='Group':
  563. isGroup='checked'
  564. isBot=''
  565. filterStr=''
  566. filterFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/filters.txt'
  567. if os.path.isfile(filterFilename):
  568. with open(filterFilename, 'r') as filterfile:
  569. filterStr=filterfile.read()
  570. blockedStr=''
  571. blockedFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/blocking.txt'
  572. if os.path.isfile(blockedFilename):
  573. with open(blockedFilename, 'r') as blockedfile:
  574. blockedStr=blockedfile.read()
  575. allowedInstancesStr=''
  576. allowedInstancesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/allowedinstances.txt'
  577. if os.path.isfile(allowedInstancesFilename):
  578. with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
  579. allowedInstancesStr=allowedInstancesFile.read()
  580. skills=getSkills(baseDir,nickname,domain)
  581. skillsStr=''
  582. skillCtr=1
  583. if skills:
  584. for skillDesc,skillValue in skills.items():
  585. skillsStr+='<p><input type="text" placeholder="'+translate['Skill']+' '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="'+skillDesc+'" style="width:40%">'
  586. skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="'+str(skillValue)+'"></p>'
  587. skillCtr+=1
  588. skillsStr+='<p><input type="text" placeholder="Skill '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="" style="width:40%">'
  589. skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="50"></p>' \
  590. cssFilename=baseDir+'/epicyon-profile.css'
  591. if os.path.isfile(baseDir+'/epicyon.css'):
  592. cssFilename=baseDir+'/epicyon.css'
  593. with open(cssFilename, 'r') as cssFile:
  594. editProfileCSS = cssFile.read()
  595. moderatorsStr=''
  596. adminNickname=getConfigParam(baseDir,'admin')
  597. if path.startswith('/users/'+adminNickname+'/'):
  598. moderators=''
  599. moderatorsFile=baseDir+'/accounts/moderators.txt'
  600. if os.path.isfile(moderatorsFile):
  601. with open(moderatorsFile, "r") as f:
  602. moderators = f.read()
  603. moderatorsStr= \
  604. '<div class="container">' \
  605. ' <b>'+translate['Moderators']+'</b><br>' \
  606. ' '+translate['A list of moderator nicknames. One per line.']+ \
  607. ' <textarea id="message" name="moderators" placeholder="'+translate['List of moderator nicknames']+'..." style="height:200px">'+moderators+'</textarea>' \
  608. '</div>'
  609. editProfileForm=htmlHeader(cssFilename,editProfileCSS)
  610. editProfileForm+= \
  611. '<form enctype="multipart/form-data" method="POST" action="'+path+'/profiledata">' \
  612. ' <div class="vertical-center">' \
  613. ' <p class="new-post-text">'+translate['Profile for']+' '+nickname+'@'+domainFull+'</p>' \
  614. ' <div class="container">' \
  615. ' <input type="submit" name="submitProfile" value="'+translate['Submit']+'">' \
  616. ' <a href="'+pathOriginal+'"><button class="cancelbtn">'+translate['Cancel']+'</button></a>' \
  617. ' </div>'+ \
  618. ' <div class="container">' \
  619. ' <input type="text" placeholder="name" name="displayNickname" value="'+displayNickname+'">' \
  620. ' <textarea id="message" name="bio" placeholder="'+translate['Your bio']+'..." style="height:200px">'+bioStr+'</textarea>' \
  621. ' </div>' \
  622. ' <div class="container">' \
  623. ' '+translate['The files attached below should be no larger than 10MB in total uploaded at once.']+'<br>' \
  624. ' '+translate['Avatar image']+ \
  625. ' <input type="file" id="avatar" name="avatar"' \
  626. ' accept=".png">' \
  627. ' <br>'+translate['Background image']+ \
  628. ' <input type="file" id="image" name="image"' \
  629. ' accept=".png">' \
  630. ' <br>'+translate['Timeline banner image']+ \
  631. ' <input type="file" id="banner" name="banner"' \
  632. ' accept=".png">' \
  633. ' </div>' \
  634. ' <div class="container">' \
  635. ' <input type="checkbox" class=profilecheckbox" name="approveFollowers" '+manuallyApprovesFollowers+'>'+translate['Approve follower requests']+'<br>' \
  636. ' <input type="checkbox" class=profilecheckbox" name="isBot" '+isBot+'>'+translate['This is a bot account']+'<br>' \
  637. ' <input type="checkbox" class=profilecheckbox" name="isGroup" '+isGroup+'>'+translate['This is a group account']+'<br>' \
  638. ' <br><b>'+translate['Filtered words']+'</b>' \
  639. ' <br>'+translate['One per line']+ \
  640. ' <textarea id="message" name="filteredWords" placeholder="" style="height:200px">'+filterStr+'</textarea>' \
  641. ' <br><b>'+translate['Blocked accounts']+'</b>' \
  642. ' <br>'+translate['Blocked accounts, one per line, in the form nickname@domain or *@blockeddomain']+ \
  643. ' <textarea id="message" name="blocked" placeholder="" style="height:200px">'+blockedStr+'</textarea>' \
  644. ' <br><b>'+translate['Federation list']+'</b>' \
  645. ' <br>'+translate['Federate only with a defined set of instances. One domain name per line.']+ \
  646. ' <textarea id="message" name="allowedInstances" placeholder="" style="height:200px">'+allowedInstancesStr+'</textarea>' \
  647. ' </div>' \
  648. ' <div class="container">' \
  649. ' <b>'+translate['Skills']+'</b><br>' \
  650. ' '+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.']+ \
  651. skillsStr+moderatorsStr+ \
  652. ' </div>' \
  653. ' </div>' \
  654. '</form>'
  655. editProfileForm+=htmlFooter()
  656. return editProfileForm
  657. def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str,bool):
  658. """Receives login credentials via HTTPServer POST
  659. """
  660. if not loginParams.startswith('username='):
  661. return None,None,None
  662. # minimum time between login attempts
  663. currTime=int(time.time())
  664. if currTime<lastLoginTime+10:
  665. return None,None,None
  666. if '&' not in loginParams:
  667. return None,None,None
  668. loginArgs=loginParams.split('&')
  669. nickname=None
  670. password=None
  671. register=False
  672. for arg in loginArgs:
  673. if '=' in arg:
  674. if arg.split('=',1)[0]=='username':
  675. nickname=arg.split('=',1)[1]
  676. elif arg.split('=',1)[0]=='password':
  677. password=arg.split('=',1)[1]
  678. elif arg.split('=',1)[0]=='register':
  679. register=True
  680. return nickname,password,register
  681. def htmlLogin(translate: {},baseDir: str) -> str:
  682. """Shows the login screen
  683. """
  684. accounts=noOfAccounts(baseDir)
  685. if not os.path.isfile(baseDir+'/accounts/login.png'):
  686. copyfile(baseDir+'/img/login.png',baseDir+'/accounts/login.png')
  687. if os.path.isfile(baseDir+'/img/login-background.png'):
  688. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  689. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  690. if accounts>0:
  691. loginText='<p class="login-text">'+translate['Welcome. Please enter your login details below.']+'</p>'
  692. else:
  693. loginText='<p class="login-text">'+translate['Please enter some credentials']+'</p>'
  694. loginText+='<p class="login-text">'+translate['You will become the admin of this site.']+'</p>'
  695. if os.path.isfile(baseDir+'/accounts/login.txt'):
  696. # custom login message
  697. with open(baseDir+'/accounts/login.txt', 'r') as file:
  698. loginText = '<p class="login-text">'+file.read()+'</p>'
  699. cssFilename=baseDir+'/epicyon-login.css'
  700. if os.path.isfile(baseDir+'/login.css'):
  701. cssFilename=baseDir+'/login.css'
  702. with open(cssFilename, 'r') as cssFile:
  703. loginCSS = cssFile.read()
  704. # show the register button
  705. registerButtonStr=''
  706. if getConfigParam(baseDir,'registration')=='open':
  707. if int(getConfigParam(baseDir,'registrationsRemaining'))>0:
  708. if accounts>0:
  709. loginText='<p class="login-text">'+translate['Welcome. Please login or register a new account.']+'</p>'
  710. registerButtonStr='<button type="submit" name="register">Register</button>'
  711. TOSstr='<p class="login-text"><a href="/terms">'+translate['Terms of Service']+'</a></p>'
  712. TOSstr+='<p class="login-text"><a href="/about">'+translate['About this Instance']+'</a></p>'
  713. loginButtonStr=''
  714. if accounts>0:
  715. loginButtonStr='<button type="submit" name="submit">'+translate['Login']+'</button>'
  716. loginForm=htmlHeader(cssFilename,loginCSS)
  717. loginForm+= \
  718. '<form method="POST" action="/login">' \
  719. ' <div class="imgcontainer">' \
  720. ' <img src="login.png" alt="login image" class="loginimage">'+ \
  721. loginText+TOSstr+ \
  722. ' </div>' \
  723. '' \
  724. ' <div class="container">' \
  725. ' <label for="nickname"><b>'+translate['Nickname']+'</b></label>' \
  726. ' <input type="text" placeholder="'+translate['Enter Nickname']+'" name="username" required autofocus>' \
  727. '' \
  728. ' <label for="password"><b>'+translate['Password']+'</b></label>' \
  729. ' <input type="password" placeholder="'+translate['Enter Password']+'" name="password" required>'+ \
  730. registerButtonStr+loginButtonStr+ \
  731. ' </div>' \
  732. '</form>'
  733. loginForm+='<a href="https://gitlab.com/bashrc2/epicyon"><img class="license" src="/icons/agpl.png" /></a>'
  734. loginForm+=htmlFooter()
  735. return loginForm
  736. def htmlTermsOfService(baseDir: str,httpPrefix: str,domainFull: str) -> str:
  737. """Show the terms of service screen
  738. """
  739. adminNickname = getConfigParam(baseDir,'admin')
  740. if not os.path.isfile(baseDir+'/accounts/tos.txt'):
  741. copyfile(baseDir+'/default_tos.txt',baseDir+'/accounts/tos.txt')
  742. if os.path.isfile(baseDir+'/img/login-background.png'):
  743. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  744. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  745. TOSText='Terms of Service go here.'
  746. if os.path.isfile(baseDir+'/accounts/tos.txt'):
  747. with open(baseDir+'/accounts/tos.txt', 'r') as file:
  748. TOSText = file.read()
  749. TOSForm=''
  750. cssFilename=baseDir+'/epicyon-profile.css'
  751. if os.path.isfile(baseDir+'/epicyon.css'):
  752. cssFilename=baseDir+'/epicyon.css'
  753. with open(cssFilename, 'r') as cssFile:
  754. termsCSS = cssFile.read()
  755. TOSForm=htmlHeader(cssFilename,termsCSS)
  756. TOSForm+='<div class="container">'+TOSText+'</div>'
  757. if adminNickname:
  758. adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
  759. TOSForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
  760. TOSForm+=htmlFooter()
  761. return TOSForm
  762. def htmlAbout(baseDir: str,httpPrefix: str,domainFull: str) -> str:
  763. """Show the about screen
  764. """
  765. adminNickname = getConfigParam(baseDir,'admin')
  766. if not os.path.isfile(baseDir+'/accounts/about.txt'):
  767. copyfile(baseDir+'/default_about.txt',baseDir+'/accounts/about.txt')
  768. if os.path.isfile(baseDir+'/img/login-background.png'):
  769. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  770. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  771. aboutText='Information about this instance goes here.'
  772. if os.path.isfile(baseDir+'/accounts/about.txt'):
  773. with open(baseDir+'/accounts/about.txt', 'r') as file:
  774. aboutText = file.read()
  775. aboutForm=''
  776. cssFilename=baseDir+'/epicyon-profile.css'
  777. if os.path.isfile(baseDir+'/epicyon.css'):
  778. cssFilename=baseDir+'/epicyon.css'
  779. with open(cssFilename, 'r') as cssFile:
  780. termsCSS = cssFile.read()
  781. aboutForm=htmlHeader(cssFilename,termsCSS)
  782. aboutForm+='<div class="container">'+aboutText+'</div>'
  783. if adminNickname:
  784. adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
  785. aboutForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
  786. aboutForm+=htmlFooter()
  787. return aboutForm
  788. def htmlHashtagBlocked(baseDir: str) -> str:
  789. """Show the screen for a blocked hashtag
  790. """
  791. blockedHashtagForm=''
  792. cssFilename=baseDir+'/epicyon-suspended.css'
  793. if os.path.isfile(baseDir+'/suspended.css'):
  794. cssFilename=baseDir+'/suspended.css'
  795. with open(cssFilename, 'r') as cssFile:
  796. blockedHashtagCSS=cssFile.read()
  797. blockedHashtagForm=htmlHeader(cssFilename,blockedHashtagCSS)
  798. blockedHashtagForm+='<div><center>'
  799. blockedHashtagForm+=' <p class="screentitle">Hashtag Blocked</p>'
  800. blockedHashtagForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
  801. blockedHashtagForm+='</center></div>'
  802. blockedHashtagForm+=htmlFooter()
  803. return blockedHashtagForm
  804. def htmlSuspended(baseDir: str) -> str:
  805. """Show the screen for suspended accounts
  806. """
  807. suspendedForm=''
  808. cssFilename=baseDir+'/epicyon-suspended.css'
  809. if os.path.isfile(baseDir+'/suspended.css'):
  810. cssFilename=baseDir+'/suspended.css'
  811. with open(cssFilename, 'r') as cssFile:
  812. suspendedCSS=cssFile.read()
  813. suspendedForm=htmlHeader(cssFilename,suspendedCSS)
  814. suspendedForm+='<div><center>'
  815. suspendedForm+=' <p class="screentitle">Account Suspended</p>'
  816. suspendedForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
  817. suspendedForm+='</center></div>'
  818. suspendedForm+=htmlFooter()
  819. return suspendedForm
  820. def htmlNewPost(translate: {},baseDir: str, \
  821. path: str,inReplyTo: str, \
  822. mentions: [], \
  823. reportUrl: str,pageNumber: int) -> str:
  824. """New post screen
  825. """
  826. iconsDir=getIconsDir(baseDir)
  827. replyStr=''
  828. if not path.endswith('/newshare'):
  829. if not path.endswith('/newreport'):
  830. if not inReplyTo:
  831. newPostText='<p class="new-post-text">'+translate['Write your post text below.']+'</p>'
  832. else:
  833. newPostText='<p class="new-post-text">'+translate['Write your reply to']+' <a href="'+inReplyTo+'">'+translate['this post']+'</a></p>'
  834. replyStr='<input type="hidden" name="replyTo" value="'+inReplyTo+'">'
  835. else:
  836. newPostText= \
  837. '<p class="new-post-text">'+translate['Write your report below.']+'</p>'
  838. # custom report header with any additional instructions
  839. if os.path.isfile(baseDir+'/accounts/report.txt'):
  840. with open(baseDir+'/accounts/report.txt', 'r') as file:
  841. customReportText=file.read()
  842. if '</p>' not in customReportText:
  843. customReportText='<p class="login-subtext">'+customReportText+'</p>'
  844. customReportText=customReportText.replace('<p>','<p class="login-subtext">')
  845. newPostText+=customReportText
  846. 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>'
  847. else:
  848. newPostText='<p class="new-post-text">'+translate['Enter the details for your shared item below.']+'</p>'
  849. if os.path.isfile(baseDir+'/accounts/newpost.txt'):
  850. with open(baseDir+'/accounts/newpost.txt', 'r') as file:
  851. newPostText = '<p class="new-post-text">'+file.read()+'</p>'
  852. cssFilename=baseDir+'/epicyon-profile.css'
  853. if os.path.isfile(baseDir+'/epicyon.css'):
  854. cssFilename=baseDir+'/epicyon.css'
  855. with open(cssFilename, 'r') as cssFile:
  856. newPostCSS = cssFile.read()
  857. if '?' in path:
  858. path=path.split('?')[0]
  859. pathBase=path.replace('/newreport','').replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','')
  860. scopeIcon='scope_public.png'
  861. scopeDescription=translate['Public']
  862. placeholderSubject=translate['Subject or Content Warning (optional)']+'...'
  863. placeholderMessage=translate['Write something']+'...'
  864. extraFields=''
  865. endpoint='newpost'
  866. if path.endswith('/newunlisted'):
  867. scopeIcon='scope_unlisted.png'
  868. scopeDescription=translate['Unlisted']
  869. endpoint='newunlisted'
  870. if path.endswith('/newfollowers'):
  871. scopeIcon='scope_followers.png'
  872. scopeDescription=translate['Followers']
  873. endpoint='newfollowers'
  874. if path.endswith('/newdm'):
  875. scopeIcon='scope_dm.png'
  876. scopeDescription=translate['DM']
  877. endpoint='newdm'
  878. if path.endswith('/newreport'):
  879. scopeIcon='scope_report.png'
  880. scopeDescription=translate['Report']
  881. endpoint='newreport'
  882. if path.endswith('/newshare'):
  883. scopeIcon='scope_share.png'
  884. scopeDescription=translate['Shared Item']
  885. placeholderSubject=translate['Name of the shared item']+'...'
  886. placeholderMessage=translate['Description of the item being shared']+'...'
  887. endpoint='newshare'
  888. extraFields= \
  889. '<div class="container">' \
  890. ' <input type="text" class="itemType" placeholder="'+translate['Type of shared item. eg. hat']+'" name="itemType">' \
  891. ' <input type="text" class="category" placeholder="'+translate['Category of shared item. eg. clothing']+'" name="category">' \
  892. ' <label class="labels">'+translate['Duration of listing in days']+':</label> <input type="number" name="duration" min="1" max="365" step="1" value="14">' \
  893. '</div>' \
  894. '<input type="text" placeholder="'+translate['City or location of the shared item']+'" name="location">'
  895. dateAndLocation=''
  896. if endpoint!='newshare' and endpoint!='newreport':
  897. dateAndLocation= \
  898. '<div class="container">' \
  899. '<p><img class="emojicalendar" src="/'+iconsDir+'/calendar.png"/>' \
  900. '<label class="labels">'+translate['Date']+': </label>' \
  901. '<input type="date" name="eventDate">' \
  902. '<label class="labelsright">'+translate['Time']+':' \
  903. '<input type="time" name="eventTime"></label></p>' \
  904. '<input type="text" placeholder="'+translate['Location']+'" name="location">' \
  905. '</div>'
  906. newPostForm=htmlHeader(cssFilename,newPostCSS)
  907. # only show the share option if this is not a reply
  908. shareOptionOnDropdown=''
  909. if not replyStr:
  910. shareOptionOnDropdown='<a href="'+pathBase+'/newshare"><img src="/'+iconsDir+'/scope_share.png"/><b>Share</b><br>'+translate['Describe a shared item']+'</a>'
  911. mentionsStr=''
  912. for m in mentions:
  913. mentionNickname=getNicknameFromActor(m)
  914. if not mentionNickname:
  915. continue
  916. mentionDomain,mentionPort=getDomainFromActor(m)
  917. if not mentionDomain:
  918. continue
  919. if mentionPort:
  920. mentionsHandle='@'+mentionNickname+'@'+mentionDomain+':'+str(mentionPort)
  921. else:
  922. mentionsHandle='@'+mentionNickname+'@'+mentionDomain
  923. if mentionsHandle not in mentionsStr:
  924. mentionsStr+=mentionsHandle+' '
  925. # build suffixes so that any replies or mentions are preserved when switching between scopes
  926. dropdownNewPostSuffix='/newpost'
  927. dropdownUnlistedSuffix='/newunlisted'
  928. dropdownFollowersSuffix='/newfollowers'
  929. dropdownDMSuffix='/newdm'
  930. dropdownReportSuffix='/newreport'
  931. if inReplyTo or mentions:
  932. dropdownNewPostSuffix=''
  933. dropdownUnlistedSuffix=''
  934. dropdownFollowersSuffix=''
  935. dropdownDMSuffix=''
  936. dropdownReportSuffix=''
  937. if inReplyTo:
  938. dropdownNewPostSuffix+='?replyto='+inReplyTo
  939. dropdownUnlistedSuffix+='?replyto='+inReplyTo
  940. dropdownFollowersSuffix+='?replyfollowers='+inReplyTo
  941. dropdownDMSuffix+='?replydm='+inReplyTo
  942. for mentionedActor in mentions:
  943. dropdownNewPostSuffix+='?mention='+mentionedActor
  944. dropdownUnlistedSuffix+='?mention='+mentionedActor
  945. dropdownFollowersSuffix+='?mention='+mentionedActor
  946. dropdownDMSuffix+='?mention='+mentionedActor
  947. dropdownReportSuffix+='?mention='+mentionedActor
  948. dropDownContent=''
  949. if not reportUrl:
  950. dropDownContent= \
  951. ' <div id="myDropdown" class="dropdown-content">' \
  952. ' <a href="'+pathBase+dropdownNewPostSuffix+'"><img src="/'+iconsDir+'/scope_public.png"/><b>'+translate['Public']+'</b><br>'+translate['Visible to anyone']+'</a>' \
  953. ' <a href="'+pathBase+dropdownUnlistedSuffix+'"><img src="/'+iconsDir+'/scope_unlisted.png"/><b>'+translate['Unlisted']+'</b><br>'+translate['Not on public timeline']+'</a>' \
  954. ' <a href="'+pathBase+dropdownFollowersSuffix+'"><img src="/'+iconsDir+'/scope_followers.png"/><b>'+translate['Followers']+'</b><br>'+translate['Only to followers']+'</a>' \
  955. ' <a href="'+pathBase+dropdownDMSuffix+'"><img src="/'+iconsDir+'/scope_dm.png"/><b>'+translate['DM']+'</b><br>'+translate['Only to mentioned people']+'</a>' \
  956. ' <a href="'+pathBase+dropdownReportSuffix+'"><img src="/'+iconsDir+'/scope_report.png"/><b>'+translate['Report']+'</b><br>'+translate['Send to moderators']+'</a>'+ \
  957. shareOptionOnDropdown+ \
  958. ' </div>'
  959. else:
  960. mentionsStr='Re: '+reportUrl+'\n\n'+mentionsStr
  961. newPostForm+= \
  962. '<form enctype="multipart/form-data" method="POST" action="'+path+'?'+endpoint+'?page='+str(pageNumber)+'">' \
  963. ' <div class="vertical-center">' \
  964. ' <label for="nickname"><b>'+newPostText+'</b></label>' \
  965. ' <div class="container">' \
  966. ' <div class="dropbtn" onclick="dropdown()">' \
  967. ' <img src="/'+iconsDir+'/'+scopeIcon+'"/><b class="scope-desc">'+scopeDescription+'</b>'+ \
  968. dropDownContent+ \
  969. ' </div>' \
  970. ' <input type="submit" name="submitPost" value="'+translate['Submit']+'">' \
  971. ' <a href="'+pathBase+'/inbox"><button class="cancelbtn">'+translate['Cancel']+'</button></a>' \
  972. ' <a href="'+pathBase+'/searchemoji"><img class="emojisearch" src="/emoji/1F601.png" title="'+translate['Search for emoji']+'" alt="'+translate['Search for emoji']+'"/></a>'+ \
  973. ' </div>'+ \
  974. replyStr+ \
  975. ' <input type="text" placeholder="'+placeholderSubject+'" name="subject">' \
  976. '' \
  977. ' <textarea id="message" name="message" placeholder="'+placeholderMessage+'" style="height:400px">'+mentionsStr+'</textarea>' \
  978. ''+extraFields+ \
  979. ' <div class="container">' \
  980. ' <input type="text" placeholder="'+translate['Image description']+'" name="imageDescription">' \
  981. ' <input type="file" id="attachpic" name="attachpic"' \
  982. ' accept=".png, .jpg, .jpeg, .gif, .mp4, .webm, .ogv, .mp3, .ogg">' \
  983. ' </div>' \
  984. ''+dateAndLocation+ \
  985. ' </div>' \
  986. '</form>'
  987. if not reportUrl:
  988. newPostForm+='<script>'+clickToDropDownScript()+cursorToEndOfMessageScript()+'</script>'
  989. newPostForm=newPostForm.replace('<body>','<body onload="focusOnMessage()">')
  990. newPostForm+=htmlFooter()
  991. return newPostForm
  992. def htmlHeader(cssFilename: str,css=None,refreshSec=0,lang='en') -> str:
  993. if refreshSec==0:
  994. meta=' <meta charset="utf-8">\n'
  995. else:
  996. meta=' <meta http-equiv="Refresh" content="'+str(refreshSec)+'" charset="utf-8">\n'
  997. if not css:
  998. if '/' in cssFilename:
  999. cssFilename=cssFilename.split('/')[-1]
  1000. htmlStr= \
  1001. '<!DOCTYPE html>\n' \
  1002. '<html lang="'+lang+'">\n'+ \
  1003. meta+ \
  1004. ' <style>\n' \
  1005. ' @import url("'+cssFilename+'");\n'+ \
  1006. ' background-color: #282c37' \
  1007. ' </style>\n' \
  1008. ' <body>\n'
  1009. else:
  1010. htmlStr= \
  1011. '<!DOCTYPE html>\n' \
  1012. '<html lang="'+lang+'">\n'+ \
  1013. meta+ \
  1014. ' <style>\n'+css+'</style>\n' \
  1015. ' <body>\n'
  1016. return htmlStr
  1017. def htmlFooter() -> str:
  1018. htmlStr= \
  1019. ' </body>\n' \
  1020. '</html>\n'
  1021. return htmlStr
  1022. def htmlProfilePosts(translate: {}, \
  1023. baseDir: str,httpPrefix: str, \
  1024. authorized: bool,ocapAlways: bool, \
  1025. nickname: str,domain: str,port: int, \
  1026. session,wfRequest: {},personCache: {}, \
  1027. projectVersion: str) -> str:
  1028. """Shows posts on the profile screen
  1029. These should only be public posts
  1030. """
  1031. iconsDir=getIconsDir(baseDir)
  1032. profileStr=''
  1033. maxItems=4
  1034. profileStr+='<script>'+contentWarningScript()+'</script>'
  1035. ctr=0
  1036. currPage=1
  1037. while ctr<maxItems and currPage<4:
  1038. outboxFeed= \
  1039. personBoxJson(session,baseDir,domain, \
  1040. port,'/users/'+nickname+'/outbox?page='+str(currPage), \
  1041. httpPrefix, \
  1042. 10, 'outbox', \
  1043. authorized, \
  1044. ocapAlways)
  1045. if not outboxFeed:
  1046. break
  1047. if len(outboxFeed['orderedItems'])==0:
  1048. break
  1049. for item in outboxFeed['orderedItems']:
  1050. if item['type']=='Create':
  1051. postStr=individualPostAsHtml(iconsDir,translate,None, \
  1052. baseDir,session,wfRequest,personCache, \
  1053. nickname,domain,port,item,None,True,False, \
  1054. httpPrefix,projectVersion,'inbox', \
  1055. False,False,False,True)
  1056. if postStr:
  1057. profileStr+=postStr
  1058. ctr+=1
  1059. if ctr>=maxItems:
  1060. break
  1061. currPage+=1
  1062. return profileStr
  1063. def htmlProfileFollowing(translate: {},baseDir: str,httpPrefix: str, \
  1064. authorized: bool,ocapAlways: bool, \
  1065. nickname: str,domain: str,port: int, \
  1066. session,wfRequest: {},personCache: {}, \
  1067. followingJson: {},projectVersion: str, \
  1068. buttons: [], \
  1069. feedName: str,actor: str, \
  1070. pageNumber: int, \
  1071. maxItemsPerPage: int) -> str:
  1072. """Shows following on the profile screen
  1073. """
  1074. profileStr=''
  1075. iconsDir=getIconsDir(baseDir)
  1076. if authorized and pageNumber:
  1077. if authorized and pageNumber>1:
  1078. # page up arrow
  1079. profileStr+= \
  1080. '<center><a href="'+actor+'/'+feedName+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"></a></center>'
  1081. for item in followingJson['orderedItems']:
  1082. profileStr+= \
  1083. individualFollowAsHtml(translate,baseDir,session, \
  1084. wfRequest,personCache, \
  1085. domain,item,authorized,nickname, \
  1086. httpPrefix,projectVersion, \
  1087. buttons)
  1088. if authorized and maxItemsPerPage and pageNumber:
  1089. if len(followingJson['orderedItems'])>=maxItemsPerPage:
  1090. # page down arrow
  1091. profileStr+= \
  1092. '<center><a href="'+actor+'/'+feedName+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"></a></center>'
  1093. return profileStr
  1094. def htmlProfileRoles(translate: {},nickname: str,domain: str,rolesJson: {}) -> str:
  1095. """Shows roles on the profile screen
  1096. """
  1097. profileStr=''
  1098. for project,rolesList in rolesJson.items():
  1099. profileStr+='<div class="roles"><h2>'+project+'</h2><div class="roles-inner">'
  1100. for role in rolesList:
  1101. profileStr+='<h3>'+role+'</h3>'
  1102. profileStr+='</div></div>'
  1103. if len(profileStr)==0:
  1104. profileStr+='<p>@'+nickname+'@'+domain+' has no roles assigned</p>'
  1105. else:
  1106. profileStr='<div>'+profileStr+'</div>'
  1107. return profileStr
  1108. def htmlProfileSkills(translate: {},nickname: str,domain: str,skillsJson: {}) -> str:
  1109. """Shows skills on the profile screen
  1110. """
  1111. profileStr=''
  1112. for skill,level in skillsJson.items():
  1113. profileStr+='<div>'+skill+'<br><div id="myProgress"><div id="myBar" style="width:'+str(level)+'%"></div></div></div><br>'
  1114. if len(profileStr)>0:
  1115. profileStr='<center><div class="skill-title">'+profileStr+'</div></center>'
  1116. return profileStr
  1117. def htmlProfileShares(translate: {},nickname: str,domain: str,sharesJson: {}) -> str:
  1118. """Shows shares on the profile screen
  1119. """
  1120. profileStr=''
  1121. for item in sharesJson['orderedItems']:
  1122. profileStr+='<div class="container">'
  1123. profileStr+='<p class="share-title">'+item['displayName']+'</p>'
  1124. if item.get('imageUrl'):
  1125. profileStr+='<a href="'+item['imageUrl']+'">'
  1126. profileStr+='<img src="'+item['imageUrl']+'" alt="'+translate['Item image']+'"></a>'
  1127. profileStr+='<p>'+item['summary']+'</p>'
  1128. profileStr+='<p><b>'+translate['Type']+':</b> '+item['itemType']+' '
  1129. profileStr+='<b>'+translate['Category']+':</b> '+item['category']+' '
  1130. profileStr+='<b>'+translate['Location']+':</b> '+item['location']+'</p>'
  1131. profileStr+='</div>'
  1132. if len(profileStr)>0:
  1133. profileStr='<div class="share-title">'+profileStr+'</div>'
  1134. return profileStr
  1135. def htmlProfile(translate: {},projectVersion: str, \
  1136. baseDir: str,httpPrefix: str,authorized: bool, \
  1137. ocapAlways: bool,profileJson: {},selected: str, \
  1138. session,wfRequest: {},personCache: {}, \
  1139. extraJson=None, \
  1140. pageNumber=None,maxItemsPerPage=None) -> str:
  1141. """Show the profile page as html
  1142. """
  1143. nickname=profileJson['preferredUsername']
  1144. if not nickname:
  1145. return ""
  1146. domain,port=getDomainFromActor(profileJson['id'])
  1147. if not domain:
  1148. return ""
  1149. displayName= \
  1150. addEmojiToDisplayName(baseDir,httpPrefix, \
  1151. nickname,domain, \
  1152. profileJson['name'],True)
  1153. domainFull=domain
  1154. if port:
  1155. domainFull=domain+':'+str(port)
  1156. profileDescription= \
  1157. addEmojiToDisplayName(baseDir,httpPrefix, \
  1158. nickname,domain, \
  1159. profileJson['summary'],False)
  1160. postsButton='button'
  1161. followingButton='button'
  1162. followersButton='button'
  1163. rolesButton='button'
  1164. skillsButton='button'
  1165. sharesButton='button'
  1166. if selected=='posts':
  1167. postsButton='buttonselected'
  1168. elif selected=='following':
  1169. followingButton='buttonselected'
  1170. elif selected=='followers':
  1171. followersButton='buttonselected'
  1172. elif selected=='roles':
  1173. rolesButton='buttonselected'
  1174. elif selected=='skills':
  1175. skillsButton='buttonselected'
  1176. elif selected=='shares':
  1177. sharesButton='buttonselected'
  1178. loginButton=''
  1179. followApprovalsSection=''
  1180. followApprovals=False
  1181. linkToTimelineStart=''
  1182. linkToTimelineEnd=''
  1183. editProfileStr=''
  1184. actor=profileJson['id']
  1185. if not authorized:
  1186. loginButton='<br><a href="/login"><button class="loginButton">'+translate['Login']+'</button></a>'
  1187. else:
  1188. editProfileStr='<a href="'+actor+'/editprofile"><button class="button"><span>'+translate['Edit']+' </span></button></a>'
  1189. linkToTimelineStart='<a href="/users/'+nickname+'/inbox" title="'+translate['Switch to timeline view']+'" alt="'+translate['Switch to timeline view']+'">'
  1190. linkToTimelineEnd='</a>'
  1191. # are there any follow requests?
  1192. followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
  1193. if os.path.isfile(followRequestsFilename):
  1194. with open(followRequestsFilename,'r') as f:
  1195. for line in f:
  1196. if len(line)>0:
  1197. followApprovals=True
  1198. followersButton='buttonhighlighted'
  1199. if selected=='followers':
  1200. followersButton='buttonselectedhighlighted'
  1201. break
  1202. if selected=='followers':
  1203. if followApprovals:
  1204. with open(followRequestsFilename,'r') as f:
  1205. for followerHandle in f:
  1206. if len(line)>0:
  1207. if '://' in followerHandle:
  1208. followerActor=followerHandle
  1209. else:
  1210. followerActor=httpPrefix+'://'+followerHandle.split('@')[1]+'/users/'+followerHandle.split('@')[0]
  1211. basePath=httpPrefix+'://'+domainFull+'/users/'+nickname
  1212. followApprovalsSection+='<div class="container">'
  1213. followApprovalsSection+='<a href="'+followerActor+'">'
  1214. followApprovalsSection+='<span class="followRequestHandle">'+followerHandle+'</span></a>'
  1215. followApprovalsSection+='<a href="'+basePath+'/followapprove='+followerHandle+'">'
  1216. followApprovalsSection+='<button class="followApprove">'+translate['Approve']+'</button></a>'
  1217. followApprovalsSection+='<a href="'+basePath+'/followdeny='+followerHandle+'">'
  1218. followApprovalsSection+='<button class="followDeny">'+translate['Deny']+'</button></a>'
  1219. followApprovalsSection+='</div>'
  1220. profileStr= \
  1221. linkToTimelineStart+ \
  1222. ' <div class="hero-image">' \
  1223. ' <div class="hero-text">'+ \
  1224. ' <img src="'+profileJson['icon']['url']+'" alt="'+nickname+'@'+domainFull+'" class="title">' \
  1225. ' <h1>'+displayName+'</h1>' \
  1226. ' <p><b>@'+nickname+'@'+domainFull+'</b></p>' \
  1227. ' <p>'+profileDescription+'</p>'+ \
  1228. loginButton+ \
  1229. ' </div>' \
  1230. '</div>'+ \
  1231. linkToTimelineEnd+ \
  1232. '<div class="container">\n' \
  1233. ' <center>' \
  1234. ' <a href="'+actor+'"><button class="'+postsButton+'"><span>'+translate['Posts']+' </span></button></a>' \
  1235. ' <a href="'+actor+'/following"><button class="'+followingButton+'"><span>'+translate['Following']+' </span></button></a>' \
  1236. ' <a href="'+actor+'/followers"><button class="'+followersButton+'"><span>'+translate['Followers']+' </span></button></a>' \
  1237. ' <a href="'+actor+'/roles"><button class="'+rolesButton+'"><span>'+translate['Roles']+' </span></button></a>' \
  1238. ' <a href="'+actor+'/skills"><button class="'+skillsButton+'"><span>'+translate['Skills']+' </span></button></a>' \
  1239. ' <a href="'+actor+'/shares"><button class="'+sharesButton+'"><span>'+translate['Shares']+' </span></button></a>'+ \
  1240. editProfileStr+ \
  1241. ' </center>' \
  1242. '</div>'
  1243. profileStr+=followApprovalsSection
  1244. cssFilename=baseDir+'/epicyon-profile.css'
  1245. if os.path.isfile(baseDir+'/epicyon.css'):
  1246. cssFilename=baseDir+'/epicyon.css'
  1247. with open(cssFilename, 'r') as cssFile:
  1248. profileStyle = cssFile.read().replace('image.png',actor+'/image.png')
  1249. licenseStr='<a href="https://gitlab.com/bashrc2/epicyon"><img class="license" src="/icons/agpl.png" /></a>'
  1250. if selected=='posts':
  1251. profileStr+= \
  1252. htmlProfilePosts(translate, \
  1253. baseDir,httpPrefix,authorized, \
  1254. ocapAlways,nickname,domain,port, \
  1255. session,wfRequest,personCache, \
  1256. projectVersion)+licenseStr
  1257. if selected=='following':
  1258. profileStr+= \
  1259. htmlProfileFollowing(translate,baseDir,httpPrefix, \
  1260. authorized,ocapAlways,nickname, \
  1261. domain,port,session, \
  1262. wfRequest,personCache,extraJson, \
  1263. projectVersion, \
  1264. ["unfollow"], \
  1265. selected,actor, \
  1266. pageNumber,maxItemsPerPage)
  1267. if selected=='followers':
  1268. profileStr+= \
  1269. htmlProfileFollowing(translate,baseDir,httpPrefix, \
  1270. authorized,ocapAlways,nickname, \
  1271. domain,port,session, \
  1272. wfRequest,personCache,extraJson, \
  1273. projectVersion, \
  1274. ["block"], \
  1275. selected,actor, \
  1276. pageNumber,maxItemsPerPage)
  1277. if selected=='roles':
  1278. profileStr+= \
  1279. htmlProfileRoles(translate,nickname,domainFull,extraJson)
  1280. if selected=='skills':
  1281. profileStr+= \
  1282. htmlProfileSkills(translate,nickname,domainFull,extraJson)
  1283. if selected=='shares':
  1284. profileStr+= \
  1285. htmlProfileShares(translate,nickname,domainFull,extraJson)+licenseStr
  1286. profileStr=htmlHeader(cssFilename,profileStyle)+profileStr+htmlFooter()
  1287. return profileStr
  1288. def individualFollowAsHtml(translate: {}, \
  1289. baseDir: str,session,wfRequest: {}, \
  1290. personCache: {},domain: str, \
  1291. followUrl: str, \
  1292. authorized: bool, \
  1293. actorNickname: str, \
  1294. httpPrefix: str, \
  1295. projectVersion: str, \
  1296. buttons=[]) -> str:
  1297. nickname=getNicknameFromActor(followUrl)
  1298. domain,port=getDomainFromActor(followUrl)
  1299. titleStr='@'+nickname+'@'+domain
  1300. avatarUrl=getPersonAvatarUrl(baseDir,followUrl,personCache)
  1301. if not avatarUrl:
  1302. avatarUrl=followUrl+'/avatar.png'
  1303. if domain not in followUrl:
  1304. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,displayName = \
  1305. getPersonBox(baseDir,session,wfRequest,personCache, \
  1306. projectVersion,httpPrefix,domain,'outbox')
  1307. if avatarUrl2:
  1308. avatarUrl=avatarUrl2
  1309. if displayName:
  1310. titleStr=displayName+' '+titleStr
  1311. buttonsStr=''
  1312. if authorized:
  1313. for b in buttons:
  1314. if b=='block':
  1315. buttonsStr+='<a href="/users/'+actorNickname+'?options='+followUrl+';1;'+avatarUrl+'"><button class="buttonunfollow">'+translate['Block']+'</button></a>'
  1316. #buttonsStr+='<a href="/users/'+actorNickname+'?block='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">'+translate['Block']+'</button></a>'
  1317. if b=='unfollow':
  1318. buttonsStr+='<a href="/users/'+actorNickname+'?options='+followUrl+';1;'+avatarUrl+'"><button class="buttonunfollow">'+translate['Unfollow']+'</button></a>'
  1319. #buttonsStr+='<a href="/users/'+actorNickname+'?unfollow='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">'+translate['Unfollow']+'</button></a>'
  1320. return \
  1321. '<div class="container">\n' \
  1322. '<a href="'+followUrl+'">' \
  1323. '<p><img src="'+avatarUrl+'" alt="Avatar">\n'+ \
  1324. titleStr+'</a>'+buttonsStr+'</p>' \
  1325. '</div>\n'
  1326. def clickToDropDownScript() -> str:
  1327. """Function run onclick to create a dropdown
  1328. """
  1329. script= \
  1330. 'function dropdown() {\n' \
  1331. ' document.getElementById("myDropdown").classList.toggle("show");\n' \
  1332. '}\n'
  1333. #'window.onclick = function(event) {' \
  1334. #" if (!event.target.matches('.dropbtn')) {" \
  1335. #' var dropdowns = document.getElementsByClassName("dropdown-content");' \
  1336. #' var i;' \
  1337. #' for (i = 0; i < dropdowns.length; i++) {' \
  1338. #' var openDropdown = dropdowns[i];' \
  1339. #" if (openDropdown.classList.contains('show')) {" \
  1340. #" openDropdown.classList.remove('show');" \
  1341. #' }' \
  1342. #' }' \
  1343. #' }' \
  1344. #'}'
  1345. return script
  1346. def cursorToEndOfMessageScript() -> str:
  1347. """Moves the cursor to the end of the text in a textarea
  1348. This avoids the cursor being in the wrong position when replying
  1349. """
  1350. script = \
  1351. 'function focusOnMessage() {\n' \
  1352. " var replyTextArea = document.getElementById('message');\n" \
  1353. ' val = replyTextArea.value;\n' \
  1354. ' if ((val.length>0) && (val.charAt(val.length-1) != " ")) {\n' \
  1355. ' val += " ";\n' \
  1356. ' }\n' \
  1357. ' replyTextArea.focus();\n' \
  1358. ' replyTextArea.value="";\n' \
  1359. ' replyTextArea.value=val;\n' \
  1360. '}\n' \
  1361. "var replyTextArea = document.getElementById('message')\n" \
  1362. 'replyTextArea.onFocus = function() {\n' \
  1363. ' focusOnMessage();' \
  1364. '}\n'
  1365. return script
  1366. def contentWarningScript() -> str:
  1367. """Returns a script used for content warnings
  1368. """
  1369. script= \
  1370. 'function showContentWarning(postID) {\n' \
  1371. ' var x = document.getElementById(postID);\n' \
  1372. ' if (x.style.display !== "block") {\n' \
  1373. ' x.style.display = "block";\n' \
  1374. ' } else {\n' \
  1375. ' x.style.display = "none";\n' \
  1376. ' }\n' \
  1377. '}\n'
  1378. return script
  1379. def addEmbeddedAudio(translate: {},content: str) -> str:
  1380. """Adds embedded audio for mp3/ogg
  1381. """
  1382. if not ('.mp3' in content or '.ogg' in content):
  1383. return content
  1384. if '<audio ' in content:
  1385. return content
  1386. extension='.mp3'
  1387. if '.ogg' in content:
  1388. extension='.ogg'
  1389. words=content.strip('\n').split(' ')
  1390. for w in words:
  1391. if extension not in w:
  1392. continue
  1393. w=w.replace('href="','').replace('">','')
  1394. if w.endswith('.'):
  1395. w=w[:-1]
  1396. if w.endswith('"'):
  1397. w=w[:-1]
  1398. if w.endswith(';'):
  1399. w=w[:-1]
  1400. if w.endswith(':'):
  1401. w=w[:-1]
  1402. if not w.endswith(extension):
  1403. continue
  1404. if not (w.startswith('http') or w.startswith('dat:') or '/' in w):
  1405. continue
  1406. url=w
  1407. content+='<center><audio controls>'
  1408. content+='<source src="'+url+'" type="audio/'+extension.replace('.','')+'">'
  1409. content+=translate['Your browser does not support the audio element.']
  1410. content+='</audio></center>'
  1411. return content
  1412. def addEmbeddedVideo(translate: {},content: str,width=400,height=300) -> str:
  1413. """Adds embedded video for mp4/webm/ogv
  1414. """
  1415. if not ('.mp4' in content or '.webm' in content or '.ogv' in content):
  1416. return content
  1417. if '<video ' in content:
  1418. return content
  1419. extension='.mp4'
  1420. if '.webm' in content:
  1421. extension='.webm'
  1422. elif '.ogv' in content:
  1423. extension='.ogv'
  1424. words=content.strip('\n').split(' ')
  1425. for w in words:
  1426. if extension not in w:
  1427. continue
  1428. w=w.replace('href="','').replace('">','')
  1429. if w.endswith('.'):
  1430. w=w[:-1]
  1431. if w.endswith('"'):
  1432. w=w[:-1]
  1433. if w.endswith(';'):
  1434. w=w[:-1]
  1435. if w.endswith(':'):
  1436. w=w[:-1]
  1437. if not w.endswith(extension):
  1438. continue
  1439. if not (w.startswith('http') or w.startswith('dat:') or '/' in w):
  1440. continue
  1441. url=w
  1442. content+='<center><video width="'+str(width)+'" height="'+str(height)+'" controls>'
  1443. content+='<source src="'+url+'" type="video/'+extension.replace('.','')+'">'
  1444. content+=translate['Your browser does not support the video element.']
  1445. content+='</video></center>'
  1446. return content
  1447. def addEmbeddedVideoFromSites(translate: {},content: str,width=400,height=300) -> str:
  1448. """Adds embedded videos
  1449. """
  1450. if '>vimeo.com/' in content:
  1451. url=content.split('>vimeo.com/')[1]
  1452. if '<' in url:
  1453. url=url.split('<')[0]
  1454. content=content+"<center><iframe src=\"https://player.vimeo.com/video/"+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1455. return content
  1456. videoSite='https://www.youtube.com'
  1457. if '"'+videoSite in content:
  1458. url=content.split('"'+videoSite)[1]
  1459. if '"' in url:
  1460. url=url.split('"')[0].replace('/watch?v=','/embed/')
  1461. content=content+"<center><iframe src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1462. return content
  1463. invidiousSites=['https://invidio.us','axqzx4s6s54s32yentfqojs3x5i7faxza6xo3ehd4bzzsg2ii4fv2iid.onion']
  1464. for videoSite in invidiousSites:
  1465. if '"'+videoSite in content:
  1466. url=content.split('"'+videoSite)[1]
  1467. if '"' in url:
  1468. url=url.split('"')[0].replace('/watch?v=','/embed/')
  1469. content=content+"<center><iframe src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1470. return content
  1471. videoSite='https://media.ccc.de'
  1472. if '"'+videoSite in content:
  1473. url=content.split('"'+videoSite)[1]
  1474. if '"' in url:
  1475. url=url.split('"')[0]
  1476. if not url.endswith('/oembed'):
  1477. url=url+'/oembed'
  1478. content=content+"<center><iframe src=\""+videoSite+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"fullscreen\" allowfullscreen></iframe></center>"
  1479. return content
  1480. # A selection of the current larger peertube sites, mostly French and German language
  1481. # These have been chosen based on reported numbers of users and the content of each has not been reviewed, so mileage could vary
  1482. 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','https://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']
  1483. for site in peerTubeSites:
  1484. if '"https://'+site in content:
  1485. url=content.split('"https://'+site)[1]
  1486. if '"' in url:
  1487. url=url.split('"')[0].replace('/watch/','/embed/')
  1488. content=content+"<center><iframe sandbox=\"allow-same-origin allow-scripts\" src=\"https://"+site+url+"\" width=\""+str(width)+"\" height=\""+str(height)+"\" frameborder=\"0\" allow=\"autoplay; fullscreen\" allowfullscreen></iframe></center>"
  1489. return content
  1490. return content
  1491. def addEmbeddedElements(translate: {},content: str) -> str:
  1492. """Adds embedded elements for various media types
  1493. """
  1494. content=addEmbeddedVideoFromSites(translate,content)
  1495. content=addEmbeddedAudio(translate,content)
  1496. return addEmbeddedVideo(translate,content)
  1497. def followerApprovalActive(baseDir: str,nickname: str,domain: str) -> bool:
  1498. """Returns true if the given account requires follower approval
  1499. """
  1500. manuallyApprovesFollowers=False
  1501. actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
  1502. if os.path.isfile(actorFilename):
  1503. loadedActor=False
  1504. tries=0
  1505. while tries<5:
  1506. try:
  1507. with open(actorFilename, 'r') as fp:
  1508. actorJson=commentjson.load(fp)
  1509. loadedActor=True
  1510. break
  1511. except Exception as e:
  1512. print(e)
  1513. time.sleep(1)
  1514. tries+=1
  1515. if loadedActor:
  1516. if actorJson.get('manuallyApprovesFollowers'):
  1517. manuallyApprovesFollowers=actorJson['manuallyApprovesFollowers']
  1518. return manuallyApprovesFollowers
  1519. def insertQuestion(translate: {}, \
  1520. nickname: str,content: str, \
  1521. postJsonObject: {},pageNumber: int) -> str:
  1522. """ Inserts question selection into a post
  1523. """
  1524. if not isQuestion(postJsonObject):
  1525. return content
  1526. if len(postJsonObject['object']['oneOf'])==0:
  1527. return content
  1528. pageNumberStr=''
  1529. if pageNumber:
  1530. pageNumberStr='?page='+str(pageNumber)
  1531. content+='<div class="question">'
  1532. content+='<form method="POST" action="/users/'+nickname+'/question'+pageNumberStr+'">'
  1533. content+='<input type="hidden" name="messageId" value="'+postJsonObject['id']+'"><br>'
  1534. for choice in postJsonObject['object']['oneOf']:
  1535. if not choice.get('type'):
  1536. continue
  1537. if not choice.get('name'):
  1538. continue
  1539. content+='<input type="radio" name="answer" value="'+choice['name']+'"> '+choice['name']+'<br><br>'
  1540. content+='<input type="submit" value="'+translate['Vote']+'" class="vote"><br><br>'
  1541. content+='</form></div>'
  1542. return content
  1543. def addEmojiToDisplayName(baseDir: str,httpPrefix: str, \
  1544. nickname: str,domain: str, \
  1545. displayName: str,inProfileName: bool) -> str:
  1546. """Adds emoji icons to display names on individual posts
  1547. """
  1548. if ':' in displayName:
  1549. displayName=displayName.replace('<p>','').replace('</p>','')
  1550. emojiTags={}
  1551. print('TAG: displayName before tags: '+displayName)
  1552. displayName= \
  1553. addHtmlTags(baseDir,httpPrefix, \
  1554. nickname,domain,displayName,[],emojiTags)
  1555. displayName=displayName.replace('<p>','').replace('</p>','')
  1556. print('TAG: displayName after tags: '+displayName)
  1557. # convert the emoji dictionary to a list
  1558. emojiTagsList=[]
  1559. for tagName,tag in emojiTags.items():
  1560. emojiTagsList.append(tag)
  1561. print('TAG: emoji tags list: '+str(emojiTagsList))
  1562. if not inProfileName:
  1563. displayName=replaceEmojiFromTags(displayName,emojiTagsList,'post header')
  1564. else:
  1565. displayName=replaceEmojiFromTags(displayName,emojiTagsList,'profile')
  1566. print('TAG: displayName after tags 2: '+displayName)
  1567. # remove any stray emoji
  1568. while ':' in displayName:
  1569. if '://' in displayName:
  1570. break
  1571. emojiStr=displayName.split(':')[1]
  1572. prevDisplayName=displayName
  1573. displayName=displayName.replace(':'+emojiStr+':','').strip()
  1574. if prevDisplayName==displayName:
  1575. break
  1576. print('TAG: displayName after tags 3: '+displayName)
  1577. print('TAG: displayName after tag replacements: '+displayName)
  1578. return displayName
  1579. def individualPostAsHtml(iconsDir: str,translate: {}, \
  1580. pageNumber: int,baseDir: str, \
  1581. session,wfRequest: {},personCache: {}, \
  1582. nickname: str,domain: str,port: int, \
  1583. postJsonObject: {}, \
  1584. avatarUrl: str, showAvatarDropdown: bool,
  1585. allowDeletion: bool, \
  1586. httpPrefix: str, projectVersion: str, \
  1587. boxName: str, \
  1588. showRepeats=True, \
  1589. showIcons=False, \
  1590. manuallyApprovesFollowers=False, \
  1591. showPublicOnly=False) -> str:
  1592. """ Shows a single post as html
  1593. """
  1594. # If this is the inbox timeline then don't show the repeat icon on any DMs
  1595. showRepeatIcon=showRepeats
  1596. showDMicon=False
  1597. if showRepeats:
  1598. if isDM(postJsonObject):
  1599. showRepeatIcon=False
  1600. showDMicon=True
  1601. titleStr=''
  1602. galleryStr=''
  1603. isAnnounced=False
  1604. if postJsonObject['type']=='Announce':
  1605. postJsonAnnounce= \
  1606. downloadAnnounce(session,baseDir,httpPrefix,nickname,domain,postJsonObject,projectVersion)
  1607. if not postJsonAnnounce:
  1608. return ''
  1609. postJsonObject=postJsonAnnounce
  1610. isAnnounced=True
  1611. if not isinstance(postJsonObject['object'], dict):
  1612. return ''
  1613. # if this post should be public then check its recipients
  1614. if showPublicOnly:
  1615. if postJsonObject['object'].get('to'):
  1616. containsPublic=False
  1617. for toAddress in postJsonObject['object']['to']:
  1618. if toAddress.endswith('#Public'):
  1619. containsPublic=True
  1620. break
  1621. if not containsPublic:
  1622. if postJsonObject['object'].get('cc'):
  1623. for toAddress in postJsonObject['object']['cc']:
  1624. if toAddress.endswith('#Public'):
  1625. containsPublic=True
  1626. break
  1627. if not containsPublic:
  1628. return ''
  1629. isModerationPost=False
  1630. if postJsonObject['object'].get('moderationStatus'):
  1631. isModerationPost=True
  1632. avatarPosition=''
  1633. containerClass='container'
  1634. containerClassIcons='containericons'
  1635. timeClass='time-right'
  1636. actorNickname=getNicknameFromActor(postJsonObject['actor'])
  1637. actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
  1638. messageId=''
  1639. if postJsonObject.get('id'):
  1640. messageId=postJsonObject['id'].replace('/activity','')
  1641. displayName=getDisplayName(baseDir,postJsonObject['actor'],personCache)
  1642. if displayName:
  1643. displayName= \
  1644. addEmojiToDisplayName(baseDir,httpPrefix, \
  1645. nickname,domain, \
  1646. displayName,False)
  1647. titleStr+='<a href="'+messageId+'">'+displayName+'</a>'
  1648. else:
  1649. titleStr+='<a href="'+messageId+'">@'+actorNickname+'@'+actorDomain+'</a>'
  1650. # Show a DM icon for DMs in the inbox timeline
  1651. if showDMicon:
  1652. titleStr=titleStr+' <img src="/'+iconsDir+'/dm.png" class="DMicon"/>'
  1653. messageIdStr=''
  1654. if messageId:
  1655. messageIdStr=';'+messageId
  1656. replyAvatarImageInPost=''
  1657. if showRepeatIcon:
  1658. if isAnnounced:
  1659. if postJsonObject['object'].get('attributedTo'):
  1660. if postJsonObject['object']['attributedTo'].startswith(postJsonObject['actor']):
  1661. titleStr+=' <img src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/>'
  1662. else:
  1663. announceNickname=getNicknameFromActor(postJsonObject['object']['attributedTo'])
  1664. if announceNickname:
  1665. announceDomain,announcePort=getDomainFromActor(postJsonObject['object']['attributedTo'])
  1666. getPersonFromCache(baseDir,postJsonObject['object']['attributedTo'],personCache)
  1667. announceDisplayName=getDisplayName(baseDir,postJsonObject['object']['attributedTo'],personCache)
  1668. if announceDisplayName:
  1669. announceDisplayName= \
  1670. addEmojiToDisplayName(baseDir,httpPrefix, \
  1671. nickname,domain, \
  1672. announceDisplayName,False)
  1673. titleStr+=' <img src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">'+announceDisplayName+'</a>'
  1674. # show avatar of person replied to
  1675. announceActor=postJsonObject['object']['attributedTo']
  1676. announceAvatarUrl=getPersonAvatarUrl(baseDir,announceActor,personCache)
  1677. if announceAvatarUrl:
  1678. replyAvatarImageInPost= \
  1679. '<div class="timeline-avatar-reply">' \
  1680. '<a href="/users/'+nickname+'?options='+announceActor+';'+str(pageNumber)+';'+announceAvatarUrl+messageIdStr+'">' \
  1681. '<img src="'+announceAvatarUrl+'" ' \
  1682. 'title="'+translate['Show options for this person']+ \
  1683. '" alt="Avatar"'+avatarPosition+'/></a></div>'
  1684. else:
  1685. titleStr+=' <img src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@'+announceNickname+'@'+announceDomain+'</a>'
  1686. else:
  1687. titleStr+=' <img src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@unattributed</a>'
  1688. else:
  1689. titleStr+=' <img src="/'+iconsDir+'/repeat_inactive.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['id']+'">@unattributed</a>'
  1690. else:
  1691. if postJsonObject['object'].get('inReplyTo'):
  1692. containerClassIcons='containericons darker'
  1693. containerClass='container darker'
  1694. #avatarPosition=' class="right"'
  1695. if postJsonObject['object']['inReplyTo'].startswith(postJsonObject['actor']):
  1696. titleStr+=' <img src="/'+iconsDir+'/reply.png" class="announceOrReply"/>'
  1697. else:
  1698. if '/statuses/' in postJsonObject['object']['inReplyTo']:
  1699. replyActor=postJsonObject['object']['inReplyTo'].split('/statuses/')[0]
  1700. replyNickname=getNicknameFromActor(replyActor)
  1701. if replyNickname:
  1702. replyDomain,replyPort=getDomainFromActor(replyActor)
  1703. if replyNickname and replyDomain:
  1704. getPersonFromCache(baseDir,replyActor,personCache)
  1705. replyDisplayName=getDisplayName(baseDir,replyActor,personCache)
  1706. if replyDisplayName:
  1707. replyDisplayName= \
  1708. addEmojiToDisplayName(baseDir,httpPrefix, \
  1709. nickname,domain, \
  1710. replyDisplayName,False)
  1711. titleStr+=' <img src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">'+replyDisplayName+'</a>'
  1712. # show avatar of person replied to
  1713. replyAvatarUrl=getPersonAvatarUrl(baseDir,replyActor,personCache)
  1714. if replyAvatarUrl:
  1715. replyAvatarImageInPost= \
  1716. '<div class="timeline-avatar-reply">' \
  1717. '<a href="/users/'+nickname+'?options='+replyActor+';'+str(pageNumber)+';'+replyAvatarUrl+messageIdStr+'">' \
  1718. '<img src="'+replyAvatarUrl+'" ' \
  1719. 'title="'+translate['Show profile']+ \
  1720. '" alt="Avatar"'+avatarPosition+'/></a></div>'
  1721. else:
  1722. titleStr+=' <img src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">@'+replyNickname+'@'+replyDomain+'</a>'
  1723. else:
  1724. titleStr+=' <img src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">@unknown</a>'
  1725. else:
  1726. postDomain=postJsonObject['object']['inReplyTo'].replace('https://','').replace('http://','').replace('dat://','')
  1727. if '/' in postDomain:
  1728. postDomain=postDomain.split('/',1)[0]
  1729. if postDomain:
  1730. titleStr+=' <img src="/'+iconsDir+'/reply.png" class="announceOrReply"/> <a href="'+postJsonObject['object']['inReplyTo']+'">'+postDomain+'</a>'
  1731. attachmentStr=''
  1732. if postJsonObject['object'].get('attachment'):
  1733. if isinstance(postJsonObject['object']['attachment'], list):
  1734. attachmentCtr=0
  1735. attachmentStr+='<div class="media">'
  1736. for attach in postJsonObject['object']['attachment']:
  1737. if attach.get('mediaType') and attach.get('url'):
  1738. mediaType=attach['mediaType']
  1739. imageDescription=''
  1740. if attach.get('name'):
  1741. imageDescription=attach['name'].replace('"',"'")
  1742. if mediaType=='image/png' or \
  1743. mediaType=='image/jpeg' or \
  1744. mediaType=='image/gif':
  1745. if attach['url'].endswith('.png') or \
  1746. attach['url'].endswith('.jpg') or \
  1747. attach['url'].endswith('.jpeg') or \
  1748. attach['url'].endswith('.gif'):
  1749. if attachmentCtr>0:
  1750. attachmentStr+='<br>'
  1751. if boxName=='tlmedia':
  1752. galleryStr+= \
  1753. '<div class="gallery">\n' \
  1754. ' <a href="'+messageId+'">\n' \
  1755. ' <img src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" width="600" height="400">\n' \
  1756. ' </a>\n</div>\n'
  1757. attachmentStr+= \
  1758. '<a href="'+attach['url']+'">' \
  1759. '<img src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment"></a>\n'
  1760. attachmentCtr+=1
  1761. elif mediaType=='video/mp4' or \
  1762. mediaType=='video/webm' or \
  1763. mediaType=='video/ogv':
  1764. extension='.mp4'
  1765. if attach['url'].endswith('.webm'):
  1766. extension='.webm'
  1767. elif attach['url'].endswith('.ogv'):
  1768. extension='.ogv'
  1769. if attach['url'].endswith(extension):
  1770. if attachmentCtr>0:
  1771. attachmentStr+='<br>'
  1772. attachmentStr+= \
  1773. '<center><video width="400" height="300" controls>' \
  1774. '<source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="video/'+extension.replace('.','')+'">'+ \
  1775. translate['Your browser does not support the video tag.']+ \
  1776. '</video></center>'
  1777. attachmentCtr+=1
  1778. elif mediaType=='audio/mpeg' or \
  1779. mediaType=='audio/ogg':
  1780. extension='.mp3'
  1781. if attach['url'].endswith('.ogg'):
  1782. extension='.ogg'
  1783. if attach['url'].endswith(extension):
  1784. if attachmentCtr>0:
  1785. attachmentStr+='<br>'
  1786. attachmentStr+= \
  1787. '<center><audio controls>' \
  1788. '<source src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment" type="audio/'+extension.replace('.','')+'">'+ \
  1789. translate['Your browser does not support the audio tag.']+ \
  1790. '</audio></center>'
  1791. attachmentCtr+=1
  1792. attachmentStr+='</div>'
  1793. #if not avatarUrl:
  1794. avatarUrl=getPersonAvatarUrl(baseDir,postJsonObject['actor'],personCache)
  1795. avatarUrl=updateAvatarImageCache(session,baseDir,httpPrefix,postJsonObject['actor'],avatarUrl,personCache)
  1796. if not avatarUrl:
  1797. avatarUrl=postJsonObject['actor']+'/avatar.png'
  1798. fullDomain=domain
  1799. if port:
  1800. if port!=80 and port!=443:
  1801. if ':' not in domain:
  1802. fullDomain=domain+':'+str(port)
  1803. if fullDomain not in postJsonObject['actor']:
  1804. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,displayName = \
  1805. getPersonBox(baseDir,session,wfRequest,personCache, \
  1806. projectVersion,httpPrefix,domain,'outbox')
  1807. if avatarUrl2:
  1808. avatarUrl=avatarUrl2
  1809. if displayName:
  1810. displayName= \
  1811. addEmojiToDisplayName(baseDir,httpPrefix, \
  1812. nickname,domain, \
  1813. displayName,False)
  1814. titleStr=displayName+' '+titleStr
  1815. avatarImageInPost= \
  1816. ' <div class="timeline-avatar">' \
  1817. ' <a href="'+postJsonObject['actor']+'">' \
  1818. ' <img src="'+avatarUrl+'" title="'+translate['Show profile']+'" alt="Avatar"'+avatarPosition+'/></a>' \
  1819. ' </div>'
  1820. if showAvatarDropdown and fullDomain+'/users/'+nickname not in postJsonObject['actor']:
  1821. avatarImageInPost= \
  1822. ' <div class="timeline-avatar">' \
  1823. ' <a href="/users/'+nickname+'?options='+postJsonObject['actor']+';'+str(pageNumber)+';'+avatarUrl+messageIdStr+'">' \
  1824. ' <img title="'+translate['Show options for this person']+'" src="'+avatarUrl+'" '+avatarPosition+'/></a>' \
  1825. ' </div>'
  1826. publishedStr=postJsonObject['object']['published']
  1827. if '.' not in publishedStr:
  1828. if '+' not in publishedStr:
  1829. datetimeObject = datetime.strptime(publishedStr,"%Y-%m-%dT%H:%M:%SZ")
  1830. else:
  1831. datetimeObject = datetime.strptime(publishedStr.split('+')[0]+'Z',"%Y-%m-%dT%H:%M:%SZ")
  1832. else:
  1833. publishedStr=publishedStr.replace('T',' ').split('.')[0]
  1834. datetimeObject = parse(publishedStr)
  1835. publishedStr=datetimeObject.strftime("%a %b %d, %H:%M")
  1836. footerStr='<span class="'+timeClass+'">'+publishedStr+'</span>\n'
  1837. pageNumberParam=''
  1838. if pageNumber:
  1839. pageNumberParam='?page='+str(pageNumber)
  1840. announceStr=''
  1841. if not isModerationPost and showRepeatIcon:
  1842. # don't allow announce/repeat of your own posts
  1843. announceIcon='repeat_inactive.png'
  1844. announceLink='repeat'
  1845. announceTitle=translate['Repeat this post']
  1846. if announcedByPerson(postJsonObject,nickname,fullDomain):
  1847. announceIcon='repeat.png'
  1848. announceLink='unrepeat'
  1849. announceTitle=translate['Undo the repeat']
  1850. announceStr= \
  1851. '<a href="/users/'+nickname+'?'+announceLink+'='+postJsonObject['object']['id']+pageNumberParam+'?tl='+boxName+'" title="'+announceTitle+'">' \
  1852. '<img src="/'+iconsDir+'/'+announceIcon+'"/></a>'
  1853. likeStr=''
  1854. if not isModerationPost:
  1855. likeIcon='like_inactive.png'
  1856. likeLink='like'
  1857. likeTitle=translate['Like this post']
  1858. if noOfLikes(postJsonObject)>0:
  1859. likeIcon='like.png'
  1860. if likedByPerson(postJsonObject,nickname,fullDomain):
  1861. likeLink='unlike'
  1862. likeTitle=translate['Undo the like']
  1863. likeStr= \
  1864. '<a href="/users/'+nickname+'?'+likeLink+'='+postJsonObject['object']['id']+pageNumberParam+'?tl='+boxName+'" title="'+likeTitle+'">' \
  1865. '<img src="/'+iconsDir+'/'+likeIcon+'"/></a>'
  1866. deleteStr=''
  1867. if allowDeletion or \
  1868. ('/'+fullDomain+'/' in postJsonObject['actor'] and \
  1869. postJsonObject['object']['id'].startswith(postJsonObject['actor'])):
  1870. if '/users/'+nickname+'/' in postJsonObject['object']['id']:
  1871. deleteStr= \
  1872. '<a href="/users/'+nickname+'?delete='+postJsonObject['object']['id']+pageNumberParam+'" title="'+translate['Delete this post']+'">' \
  1873. '<img src="/'+iconsDir+'/delete.png"/></a>'
  1874. # change the background color for DMs in inbox timeline
  1875. if showDMicon:
  1876. containerClassIcons='containericons dm'
  1877. containerClass='container dm'
  1878. if showIcons:
  1879. replyToLink=postJsonObject['object']['id']
  1880. if postJsonObject['object'].get('attributedTo'):
  1881. replyToLink+='?mention='+postJsonObject['object']['attributedTo']
  1882. if postJsonObject['object'].get('content'):
  1883. mentionedActors=getMentionsFromHtml(postJsonObject['object']['content'])
  1884. if mentionedActors:
  1885. for actorUrl in mentionedActors:
  1886. if '?mention='+actorUrl not in replyToLink:
  1887. replyToLink+='?mention='+actorUrl
  1888. if len(replyToLink)>500:
  1889. break
  1890. replyToLink+=pageNumberParam
  1891. footerStr='<div class="'+containerClassIcons+'">'
  1892. if not isModerationPost and showRepeatIcon:
  1893. if not manuallyApprovesFollowers:
  1894. footerStr+='<a href="/users/'+nickname+'?replyto='+replyToLink+'" title="'+translate['Reply to this post']+'">'
  1895. else:
  1896. footerStr+='<a href="/users/'+nickname+'?replyfollowers='+replyToLink+'" title="'+translate['Reply to this post']+'">'
  1897. else:
  1898. footerStr+='<a href="/users/'+nickname+'?replydm='+replyToLink+'" title="'+translate['Reply to this post']+'">'
  1899. footerStr+='<img src="/'+iconsDir+'/reply.png"/></a>'
  1900. footerStr+=announceStr+likeStr+deleteStr
  1901. footerStr+='<span class="'+timeClass+'">'+publishedStr+'</span>'
  1902. footerStr+='</div>'
  1903. if not postJsonObject['object']['sensitive']:
  1904. contentStr=postJsonObject['object']['content']+attachmentStr
  1905. contentStr=addEmbeddedElements(translate,contentStr)
  1906. contentStr=insertQuestion(translate,nickname,contentStr,postJsonObject,pageNumber)
  1907. else:
  1908. postID='post'+str(createPassword(8))
  1909. contentStr=''
  1910. if postJsonObject['object'].get('summary'):
  1911. contentStr+='<b>'+postJsonObject['object']['summary']+'</b> '
  1912. if isModerationPost:
  1913. containerClass='container report'
  1914. else:
  1915. contentStr+='<b>Sensitive</b> '
  1916. contentStr+='<button class="cwButton" onclick="showContentWarning('+"'"+postID+"'"+')">'+translate['SHOW MORE']+'</button>'
  1917. contentStr+='<div class="cwText" id="'+postID+'">'
  1918. contentStr+=postJsonObject['object']['content']+attachmentStr
  1919. contentStr=addEmbeddedElements(translate,contentStr)
  1920. contentStr=insertQuestion(translate,nickname,contentStr,postJsonObject,pageNumber)
  1921. contentStr+='</div>'
  1922. if postJsonObject['object'].get('tag'):
  1923. contentStr=replaceEmojiFromTags(contentStr,postJsonObject['object']['tag'],'content')
  1924. contentStr='<div class="message">'+contentStr+'</div>'
  1925. if boxName!='tlmedia':
  1926. return \
  1927. '<div class="'+containerClass+'">\n'+ \
  1928. avatarImageInPost+ \
  1929. '<p class="post-title">'+titleStr+replyAvatarImageInPost+'</p>'+ \
  1930. contentStr+footerStr+ \
  1931. '</div>\n'
  1932. else:
  1933. return galleryStr
  1934. def isQuestion(postObjectJson: {}) -> bool:
  1935. """ is the given post a question?
  1936. """
  1937. if postObjectJson['type']=='Create':
  1938. if isinstance(postObjectJson['object'], dict):
  1939. if postObjectJson['object'].get('type'):
  1940. if postObjectJson['object']['type']=='Question':
  1941. if postObjectJson['object'].get('oneOf'):
  1942. if isinstance(postObjectJson['object']['oneOf'], list):
  1943. return True
  1944. return False
  1945. def htmlTimeline(translate: {},pageNumber: int, \
  1946. itemsPerPage: int,session,baseDir: str, \
  1947. wfRequest: {},personCache: {}, \
  1948. nickname: str,domain: str,port: int,timelineJson: {}, \
  1949. boxName: str,allowDeletion: bool, \
  1950. httpPrefix: str,projectVersion: str, \
  1951. manuallyApproveFollowers: bool) -> str:
  1952. """Show the timeline as html
  1953. """
  1954. accountDir=baseDir+'/accounts/'+nickname+'@'+domain
  1955. # should the calendar icon be highlighted?
  1956. calendarImage='calendar.png'
  1957. calendarPath='/calendar'
  1958. calendarFile=accountDir+'/.newCalendar'
  1959. if os.path.isfile(calendarFile):
  1960. calendarImage='calendar_notify.png'
  1961. with open(calendarFile, 'r') as calfile:
  1962. calendarPath=calfile.read().replace('##sent##','').replace('\n', '')
  1963. # should the DM button be highlighted?
  1964. newDM=False
  1965. dmFile=accountDir+'/.newDM'
  1966. if os.path.isfile(dmFile):
  1967. newDM=True
  1968. if boxName=='dm':
  1969. os.remove(dmFile)
  1970. # should the Replies button be highlighted?
  1971. newReply=False
  1972. replyFile=accountDir+'/.newReply'
  1973. if os.path.isfile(replyFile):
  1974. newReply=True
  1975. if boxName=='tlreplies':
  1976. os.remove(replyFile)
  1977. iconsDir=getIconsDir(baseDir)
  1978. cssFilename=baseDir+'/epicyon-profile.css'
  1979. if os.path.isfile(baseDir+'/epicyon.css'):
  1980. cssFilename=baseDir+'/epicyon.css'
  1981. with open(cssFilename, 'r') as cssFile:
  1982. profileStyle = \
  1983. cssFile.read().replace('banner.png', \
  1984. '/users/'+nickname+'/banner.png')
  1985. moderator=isModerator(baseDir,nickname)
  1986. inboxButton='button'
  1987. dmButton='button'
  1988. if newDM:
  1989. dmButton='buttonhighlighted'
  1990. repliesButton='button'
  1991. if newReply:
  1992. repliesButton='buttonhighlighted'
  1993. mediaButton='button'
  1994. sentButton='button'
  1995. moderationButton='button'
  1996. if boxName=='inbox':
  1997. inboxButton='buttonselected'
  1998. elif boxName=='dm':
  1999. dmButton='buttonselected'
  2000. if newDM:
  2001. dmButton='buttonselectedhighlighted'
  2002. elif boxName=='tlreplies':
  2003. repliesButton='buttonselected'
  2004. if newReply:
  2005. repliesButton='buttonselectedhighlighted'
  2006. elif boxName=='tlmedia':
  2007. mediaButton='buttonselected'
  2008. elif boxName=='outbox':
  2009. sentButton='buttonselected'
  2010. elif boxName=='moderation':
  2011. moderationButton='buttonselected'
  2012. actor='/users/'+nickname
  2013. showIndividualPostIcons=True
  2014. if boxName=='inbox':
  2015. showIndividualPostIcons=True
  2016. followApprovals=''
  2017. followRequestsFilename= \
  2018. baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
  2019. if os.path.isfile(followRequestsFilename):
  2020. with open(followRequestsFilename,'r') as f:
  2021. for line in f:
  2022. if len(line)>0:
  2023. # show follow approvals icon
  2024. followApprovals='<a href="'+actor+'/followers"><img class="timelineicon" alt="'+translate['Approve follow requests']+'" title="'+translate['Approve follow requests']+'" src="/'+iconsDir+'/person.png"/></a>'
  2025. break
  2026. moderationButtonStr=''
  2027. if moderator:
  2028. moderationButtonStr='<a href="'+actor+'/moderation"><button class="'+moderationButton+'"><span>'+translate['Mod']+' </span></button></a>'
  2029. tlStr=htmlHeader(cssFilename,profileStyle)
  2030. #if (boxName=='inbox' or boxName=='dm') and pageNumber==1:
  2031. # refresh if on the first page of the inbox and dm timeline
  2032. #tlStr=htmlHeader(cssFilename,profileStyle,240)
  2033. if boxName!='dm':
  2034. if not manuallyApproveFollowers:
  2035. newPostButtonStr='<a href="'+actor+'/newpost"><img src="/'+iconsDir+'/newpost.png" title="'+translate['Create a new post']+'" alt="'+translate['Create a new post']+'" class="timelineicon"/></a>'
  2036. else:
  2037. newPostButtonStr='<a href="'+actor+'/newfollowers"><img src="/'+iconsDir+'/newpost.png" title="'+translate['Create a new post']+'" alt="'+translate['Create a new post']+'" class="timelineicon"/></a>'
  2038. else:
  2039. newPostButtonStr='<a href="'+actor+'/newdm"><img src="/'+iconsDir+'/newpost.png" title="'+translate['Create a new DM']+'" alt="'+translate['Create a new DM']+'" class="timelineicon"/></a>'
  2040. # banner and row of buttons
  2041. tlStr+= \
  2042. '<a href="/users/'+nickname+'" title="'+translate['Switch to profile view']+'" alt="'+translate['Switch to profile view']+'">' \
  2043. '<div class="timeline-banner">' \
  2044. '</div></a>' \
  2045. '<div class="container">\n'+ \
  2046. ' <a href="'+actor+'/inbox"><button class="'+inboxButton+'"><span>'+translate['Inbox']+'</span></button></a>' \
  2047. ' <a href="'+actor+'/dm"><button class="'+dmButton+'"><span>'+translate['DM']+'</span></button></a>' \
  2048. ' <a href="'+actor+'/tlreplies"><button class="'+repliesButton+'"><span>'+translate['Replies']+'</span></button></a>' \
  2049. ' <a href="'+actor+'/tlmedia"><button class="'+mediaButton+'"><span>'+translate['Media']+'</span></button></a>' \
  2050. ' <a href="'+actor+'/outbox"><button class="'+sentButton+'"><span>'+translate['Outbox']+'</span></button></a>'+ \
  2051. moderationButtonStr+newPostButtonStr+ \
  2052. ' <a href="'+actor+'/search"><img src="/'+iconsDir+'/search.png" title="'+translate['Search and follow']+'" alt="'+translate['Search and follow']+'" class="timelineicon"/></a>'+ \
  2053. ' <a href="'+actor+calendarPath+'"><img src="/'+iconsDir+'/'+calendarImage+'" title="'+translate['Calendar']+'" alt="'+translate['Calendar']+'" class="timelineicon"/></a>'+ \
  2054. ' <a href="'+actor+'/'+boxName+'"><img src="/'+iconsDir+'/refresh.png" title="'+translate['Refresh']+'" alt="'+translate['Refresh']+'" class="timelineicon"/></a>'+ \
  2055. followApprovals+ \
  2056. '</div>'
  2057. # second row of buttons for moderator actions
  2058. if moderator and boxName=='moderation':
  2059. tlStr+= \
  2060. '<form method="POST" action="/users/'+nickname+'/moderationaction">' \
  2061. '<div class="container">\n'+ \
  2062. ' <input type="text" placeholder="'+translate['Nickname or URL. Block using *@domain or nickname@domain']+'" name="moderationAction" value="" autofocus>' \
  2063. ' <input type="submit" title="'+translate['Remove the above item']+'" name="submitRemove" value="'+translate['Remove']+'">' \
  2064. ' <input type="submit" title="'+translate['Suspend the above account nickname']+'" name="submitSuspend" value="'+translate['Suspend']+'">' \
  2065. ' <input type="submit" title="'+translate['Remove a suspension for an account nickname']+'" name="submitUnsuspend" value="'+translate['Unsuspend']+'">' \
  2066. ' <input type="submit" title="'+translate['Block an account on another instance']+'" name="submitBlock" value="'+translate['Block']+'">' \
  2067. ' <input type="submit" title="'+translate['Unblock an account on another instance']+'" name="submitUnblock" value="'+translate['Unblock']+'">' \
  2068. ' <input type="submit" title="'+translate['Information about current blocks/suspensions']+'" name="submitInfo" value="'+translate['Info']+'">' \
  2069. '</div></form>'
  2070. # add the javascript for content warnings
  2071. tlStr+='<script>'+contentWarningScript()+'</script>'
  2072. # page up arrow
  2073. if pageNumber>1:
  2074. tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/'+iconsDir+'/pageup.png" title="'+translate['Page up']+'" alt="'+translate['Page up']+'"></a></center>'
  2075. # show the posts
  2076. itemCtr=0
  2077. if timelineJson:
  2078. if boxName=='tlmedia':
  2079. if pageNumber>1:
  2080. tlStr+='<br>'
  2081. tlStr+='<div class="galleryContainer">\n'
  2082. for item in timelineJson['orderedItems']:
  2083. if item['type']=='Create' or item['type']=='Announce':
  2084. avatarUrl=getPersonAvatarUrl(baseDir,item['actor'],personCache)
  2085. itemCtr+=1
  2086. currTlStr= \
  2087. individualPostAsHtml(iconsDir,translate,pageNumber, \
  2088. baseDir,session,wfRequest,personCache, \
  2089. nickname,domain,port,item,avatarUrl,True, \
  2090. allowDeletion, \
  2091. httpPrefix,projectVersion,boxName, \
  2092. boxName!='dm', \
  2093. showIndividualPostIcons, \
  2094. manuallyApproveFollowers,False)
  2095. if currTlStr:
  2096. tlStr+=currTlStr
  2097. if boxName=='tlmedia':
  2098. tlStr+='</div>\n'
  2099. # page down arrow
  2100. if itemCtr>=itemsPerPage:
  2101. tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/'+iconsDir+'/pagedown.png" title="'+translate['Page down']+'" alt="'+translate['Page down']+'"></a></center>'
  2102. tlStr+=htmlFooter()
  2103. return tlStr
  2104. def htmlInbox(translate: {},pageNumber: int,itemsPerPage: int, \
  2105. session,baseDir: str,wfRequest: {},personCache: {}, \
  2106. nickname: str,domain: str,port: int,inboxJson: {}, \
  2107. allowDeletion: bool, \
  2108. httpPrefix: str,projectVersion: str) -> str:
  2109. """Show the inbox as html
  2110. """
  2111. manuallyApproveFollowers= \
  2112. followerApprovalActive(baseDir,nickname,domain)
  2113. return htmlTimeline(translate,pageNumber, \
  2114. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2115. nickname,domain,port,inboxJson,'inbox',allowDeletion, \
  2116. httpPrefix,projectVersion,manuallyApproveFollowers)
  2117. def htmlInboxDMs(translate: {},pageNumber: int,itemsPerPage: int, \
  2118. session,baseDir: str,wfRequest: {},personCache: {}, \
  2119. nickname: str,domain: str,port: int,inboxJson: {}, \
  2120. allowDeletion: bool, \
  2121. httpPrefix: str,projectVersion: str) -> str:
  2122. """Show the DM timeline as html
  2123. """
  2124. return htmlTimeline(translate,pageNumber, \
  2125. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2126. nickname,domain,port,inboxJson,'dm',allowDeletion, \
  2127. httpPrefix,projectVersion,False)
  2128. def htmlInboxReplies(translate: {},pageNumber: int,itemsPerPage: int, \
  2129. session,baseDir: str,wfRequest: {},personCache: {}, \
  2130. nickname: str,domain: str,port: int,inboxJson: {}, \
  2131. allowDeletion: bool, \
  2132. httpPrefix: str,projectVersion: str) -> str:
  2133. """Show the replies timeline as html
  2134. """
  2135. return htmlTimeline(translate,pageNumber, \
  2136. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2137. nickname,domain,port,inboxJson,'tlreplies',allowDeletion, \
  2138. httpPrefix,projectVersion,False)
  2139. def htmlInboxMedia(translate: {},pageNumber: int,itemsPerPage: int, \
  2140. session,baseDir: str,wfRequest: {},personCache: {}, \
  2141. nickname: str,domain: str,port: int,inboxJson: {}, \
  2142. allowDeletion: bool, \
  2143. httpPrefix: str,projectVersion: str) -> str:
  2144. """Show the media timeline as html
  2145. """
  2146. return htmlTimeline(translate,pageNumber, \
  2147. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2148. nickname,domain,port,inboxJson,'tlmedia',allowDeletion, \
  2149. httpPrefix,projectVersion,False)
  2150. def htmlModeration(translate: {},pageNumber: int,itemsPerPage: int, \
  2151. session,baseDir: str,wfRequest: {},personCache: {}, \
  2152. nickname: str,domain: str,port: int,inboxJson: {}, \
  2153. allowDeletion: bool, \
  2154. httpPrefix: str,projectVersion: str) -> str:
  2155. """Show the moderation feed as html
  2156. """
  2157. return htmlTimeline(translate,pageNumber, \
  2158. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2159. nickname,domain,port,inboxJson,'moderation',allowDeletion, \
  2160. httpPrefix,projectVersion,True)
  2161. def htmlOutbox(translate: {},pageNumber: int,itemsPerPage: int, \
  2162. session,baseDir: str,wfRequest: {},personCache: {}, \
  2163. nickname: str,domain: str,port: int,outboxJson: {}, \
  2164. allowDeletion: bool,
  2165. httpPrefix: str,projectVersion: str) -> str:
  2166. """Show the Outbox as html
  2167. """
  2168. manuallyApproveFollowers= \
  2169. followerApprovalActive(baseDir,nickname,domain)
  2170. return htmlTimeline(translate,pageNumber, \
  2171. itemsPerPage,session,baseDir,wfRequest,personCache, \
  2172. nickname,domain,port,outboxJson,'outbox',allowDeletion, \
  2173. httpPrefix,projectVersion,manuallyApproveFollowers)
  2174. def htmlIndividualPost(translate: {}, \
  2175. baseDir: str,session,wfRequest: {},personCache: {}, \
  2176. nickname: str,domain: str,port: int,authorized: bool, \
  2177. postJsonObject: {},httpPrefix: str,projectVersion: str) -> str:
  2178. """Show an individual post as html
  2179. """
  2180. iconsDir=getIconsDir(baseDir)
  2181. postStr='<script>'+contentWarningScript()+'</script>'
  2182. postStr+= \
  2183. individualPostAsHtml(iconsDir,translate,None, \
  2184. baseDir,session,wfRequest,personCache, \
  2185. nickname,domain,port,postJsonObject,None,True,False, \
  2186. httpPrefix,projectVersion,'inbox', \
  2187. False,authorized,False,False)
  2188. messageId=postJsonObject['id'].replace('/activity','')
  2189. # show the previous posts
  2190. while postJsonObject['object'].get('inReplyTo'):
  2191. postFilename=locatePost(baseDir,nickname,domain,postJsonObject['object']['inReplyTo'])
  2192. if not postFilename:
  2193. break
  2194. loadedPost=False
  2195. tries=0
  2196. while tries<5:
  2197. try:
  2198. with open(postFilename, 'r') as fp:
  2199. postJsonObject=commentjson.load(fp)
  2200. loadedPost=True
  2201. break
  2202. except Exception as e:
  2203. print(e)
  2204. time.sleep(1)
  2205. tries+=1
  2206. if loadedPost:
  2207. postStr= \
  2208. individualPostAsHtml(iconsDir,translate,None, \
  2209. baseDir,session,wfRequest,personCache, \
  2210. nickname,domain,port,postJsonObject, \
  2211. None,True,False, \
  2212. httpPrefix,projectVersion,'inbox', \
  2213. False,authorized,False,False)+postStr
  2214. # show the following posts
  2215. postFilename=locatePost(baseDir,nickname,domain,messageId)
  2216. if postFilename:
  2217. # is there a replies file for this post?
  2218. repliesFilename=postFilename.replace('.json','.replies')
  2219. if os.path.isfile(repliesFilename):
  2220. # get items from the replies file
  2221. repliesJson={'orderedItems': []}
  2222. populateRepliesJson(baseDir,nickname,domain,repliesFilename,authorized,repliesJson)
  2223. # add items to the html output
  2224. for item in repliesJson['orderedItems']:
  2225. postStr+= \
  2226. individualPostAsHtml(iconsDir,translate,None, \
  2227. baseDir,session,wfRequest,personCache, \
  2228. nickname,domain,port,item,None,True,False, \
  2229. httpPrefix,projectVersion,'inbox', \
  2230. False,authorized,False,False)
  2231. cssFilename=baseDir+'/epicyon-profile.css'
  2232. if os.path.isfile(baseDir+'/epicyon.css'):
  2233. cssFilename=baseDir+'/epicyon.css'
  2234. with open(cssFilename, 'r') as cssFile:
  2235. postsCSS=cssFile.read()
  2236. return htmlHeader(cssFilename,postsCSS)+postStr+htmlFooter()
  2237. def htmlPostReplies(translate: {},baseDir: str, \
  2238. session,wfRequest: {},personCache: {}, \
  2239. nickname: str,domain: str,port: int,repliesJson: {}, \
  2240. httpPrefix: str,projectVersion: str) -> str:
  2241. """Show the replies to an individual post as html
  2242. """
  2243. iconsDir=getIconsDir(baseDir)
  2244. repliesStr=''
  2245. if repliesJson.get('orderedItems'):
  2246. for item in repliesJson['orderedItems']:
  2247. repliesStr+= \
  2248. individualPostAsHtml(iconsDir,translate,None, \
  2249. baseDir,session,wfRequest,personCache, \
  2250. nickname,domain,port,item,None,True,False, \
  2251. httpPrefix,projectVersion,'inbox', \
  2252. False,False,False,False)
  2253. cssFilename=baseDir+'/epicyon-profile.css'
  2254. if os.path.isfile(baseDir+'/epicyon.css'):
  2255. cssFilename=baseDir+'/epicyon.css'
  2256. with open(cssFilename, 'r') as cssFile:
  2257. postsCSS=cssFile.read()
  2258. return htmlHeader(cssFilename,postsCSS)+repliesStr+htmlFooter()
  2259. def htmlRemoveSharedItem(translate: {},baseDir: str,actor: str,shareName: str) -> str:
  2260. """Shows a screen asking to confirm the removal of a shared item
  2261. """
  2262. nickname=getNicknameFromActor(actor)
  2263. domain,port=getDomainFromActor(actor)
  2264. sharesFile=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json'
  2265. if not os.path.isfile(sharesFile):
  2266. return None
  2267. sharesJson=None
  2268. tries=0
  2269. while tries<5:
  2270. try:
  2271. with open(sharesFile, 'r') as fp:
  2272. sharesJson=commentjson.load(fp)
  2273. break
  2274. except Exception as e:
  2275. print(e)
  2276. time.sleep(1)
  2277. tries+=1
  2278. if not sharesJson:
  2279. return None
  2280. if not sharesJson.get(shareName):
  2281. return None
  2282. sharedItemDisplayName=sharesJson[shareName]['displayName']
  2283. sharedItemImageUrl=None
  2284. if sharesJson[shareName].get('imageUrl'):
  2285. sharedItemImageUrl=sharesJson[shareName]['imageUrl']
  2286. if os.path.isfile(baseDir+'/img/shares-background.png'):
  2287. if not os.path.isfile(baseDir+'/accounts/shares-background.png'):
  2288. copyfile(baseDir+'/img/shares-background.png',baseDir+'/accounts/shares-background.png')
  2289. cssFilename=baseDir+'/epicyon-follow.css'
  2290. if os.path.isfile(baseDir+'/follow.css'):
  2291. cssFilename=baseDir+'/follow.css'
  2292. with open(cssFilename, 'r') as cssFile:
  2293. profileStyle = cssFile.read()
  2294. sharesStr=htmlHeader(cssFilename,profileStyle)
  2295. sharesStr+='<div class="follow">'
  2296. sharesStr+=' <div class="followAvatar">'
  2297. sharesStr+=' <center>'
  2298. if sharedItemImageUrl:
  2299. sharesStr+=' <img src="'+sharedItemImageUrl+'"/>'
  2300. sharesStr+=' <p class="followText">'+translate['Remove']+' '+sharedItemDisplayName+' ?</p>'
  2301. sharesStr+= \
  2302. ' <form method="POST" action="'+actor+'/rmshare">' \
  2303. ' <input type="hidden" name="actor" value="'+actor+'">' \
  2304. ' <input type="hidden" name="shareName" value="'+shareName+'">' \
  2305. ' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>' \
  2306. ' <a href="'+actor+'/inbox'+'"><button class="button">'+translate['No']+'</button></a>' \
  2307. ' </form>'
  2308. sharesStr+=' </center>'
  2309. sharesStr+=' </div>'
  2310. sharesStr+='</div>'
  2311. sharesStr+=htmlFooter()
  2312. return sharesStr
  2313. def htmlDeletePost(translate,pageNumber: int, \
  2314. session,baseDir: str,messageId: str, \
  2315. httpPrefix: str,projectVersion: str, \
  2316. wfRequest: {},personCache: {}) -> str:
  2317. """Shows a screen asking to confirm the deletion of a post
  2318. """
  2319. if '/statuses/' not in messageId:
  2320. return None
  2321. iconsDir=getIconsDir(baseDir)
  2322. actor=messageId.split('/statuses/')[0]
  2323. nickname=getNicknameFromActor(actor)
  2324. domain,port=getDomainFromActor(actor)
  2325. postFilename=locatePost(baseDir,nickname,domain,messageId)
  2326. if not postFilename:
  2327. return None
  2328. postJsonObject=None
  2329. tries=0
  2330. while tries<5:
  2331. try:
  2332. with open(postFilename, 'r') as fp:
  2333. postJsonObject=commentjson.load(fp)
  2334. break
  2335. except Exception as e:
  2336. print(e)
  2337. time.sleep(1)
  2338. tries+=1
  2339. if not postJsonObject:
  2340. return None
  2341. if os.path.isfile(baseDir+'/img/delete-background.png'):
  2342. if not os.path.isfile(baseDir+'/accounts/delete-background.png'):
  2343. copyfile(baseDir+'/img/delete-background.png', \
  2344. baseDir+'/accounts/delete-background.png')
  2345. deletePostStr=None
  2346. cssFilename=baseDir+'/epicyon-profile.css'
  2347. if os.path.isfile(baseDir+'/epicyon.css'):
  2348. cssFilename=baseDir+'/epicyon.css'
  2349. with open(cssFilename, 'r') as cssFile:
  2350. profileStyle = cssFile.read()
  2351. deletePostStr=htmlHeader(cssFilename,profileStyle)
  2352. deletePostStr+='<script>'+contentWarningScript()+'</script>'
  2353. deletePostStr+= \
  2354. individualPostAsHtml(iconsDir,translate,pageNumber, \
  2355. baseDir,session,wfRequest,personCache, \
  2356. nickname,domain,port,postJsonObject, \
  2357. None,True,False, \
  2358. httpPrefix,projectVersion,'outbox', \
  2359. False,False,False,False)
  2360. deletePostStr+='<center>'
  2361. deletePostStr+=' <p class="followText">'+translate['Delete this post?']+'</p>'
  2362. deletePostStr+= \
  2363. ' <form method="POST" action="'+actor+'/rmpost">' \
  2364. ' <input type="hidden" name="pageNumber" value="'+str(pageNumber)+'">' \
  2365. ' <input type="hidden" name="messageId" value="'+messageId+'">' \
  2366. ' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>' \
  2367. ' <a href="'+actor+'/inbox'+'"><button class="button">'+translate['No']+'</button></a>' \
  2368. ' </form>'
  2369. deletePostStr+='</center>'
  2370. deletePostStr+=htmlFooter()
  2371. return deletePostStr
  2372. def htmlFollowConfirm(translate: {},baseDir: str, \
  2373. originPathStr: str, \
  2374. followActor: str, \
  2375. followProfileUrl: str) -> str:
  2376. """Asks to confirm a follow
  2377. """
  2378. followDomain,port=getDomainFromActor(followActor)
  2379. if os.path.isfile(baseDir+'/img/follow-background.png'):
  2380. if not os.path.isfile(baseDir+'/accounts/follow-background.png'):
  2381. copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png')
  2382. cssFilename=baseDir+'/epicyon-follow.css'
  2383. if os.path.isfile(baseDir+'/follow.css'):
  2384. cssFilename=baseDir+'/follow.css'
  2385. with open(cssFilename, 'r') as cssFile:
  2386. profileStyle = cssFile.read()
  2387. followStr=htmlHeader(cssFilename,profileStyle)
  2388. followStr+='<div class="follow">'
  2389. followStr+=' <div class="followAvatar">'
  2390. followStr+=' <center>'
  2391. followStr+=' <a href="'+followActor+'">'
  2392. followStr+=' <img src="'+followProfileUrl+'"/></a>'
  2393. followStr+=' <p class="followText">'+translate['Follow']+' '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
  2394. followStr+= \
  2395. ' <form method="POST" action="'+originPathStr+'/followconfirm">' \
  2396. ' <input type="hidden" name="actor" value="'+followActor+'">' \
  2397. ' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>' \
  2398. ' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>' \
  2399. ' </form>'
  2400. followStr+='</center>'
  2401. followStr+='</div>'
  2402. followStr+='</div>'
  2403. followStr+=htmlFooter()
  2404. return followStr
  2405. def htmlUnfollowConfirm(translate: {},baseDir: str, \
  2406. originPathStr: str, \
  2407. followActor: str, \
  2408. followProfileUrl: str) -> str:
  2409. """Asks to confirm unfollowing an actor
  2410. """
  2411. followDomain,port=getDomainFromActor(followActor)
  2412. if os.path.isfile(baseDir+'/img/follow-background.png'):
  2413. if not os.path.isfile(baseDir+'/accounts/follow-background.png'):
  2414. copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png')
  2415. cssFilename=baseDir+'/epicyon-follow.css'
  2416. if os.path.isfile(baseDir+'/follow.css'):
  2417. cssFilename=baseDir+'/follow.css'
  2418. with open(cssFilename, 'r') as cssFile:
  2419. profileStyle = cssFile.read()
  2420. followStr=htmlHeader(cssFilename,profileStyle)
  2421. followStr+='<div class="follow">'
  2422. followStr+=' <div class="followAvatar">'
  2423. followStr+=' <center>'
  2424. followStr+=' <a href="'+followActor+'">'
  2425. followStr+=' <img src="'+followProfileUrl+'"/></a>'
  2426. followStr+=' <p class="followText">'+translate['Stop following']+' '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
  2427. followStr+= \
  2428. ' <form method="POST" action="'+originPathStr+'/unfollowconfirm">' \
  2429. ' <input type="hidden" name="actor" value="'+followActor+'">' \
  2430. ' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>' \
  2431. ' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>' \
  2432. ' </form>'
  2433. followStr+='</center>'
  2434. followStr+='</div>'
  2435. followStr+='</div>'
  2436. followStr+=htmlFooter()
  2437. return followStr
  2438. def htmlPersonOptions(translate: {},baseDir: str, \
  2439. domain: str,originPathStr: str, \
  2440. optionsActor: str, \
  2441. optionsProfileUrl: str, \
  2442. optionsLink: str, \
  2443. pageNumber: int) -> str:
  2444. """Show options for a person: view/follow/block/report
  2445. """
  2446. optionsDomain,optionsPort=getDomainFromActor(optionsActor)
  2447. if os.path.isfile(baseDir+'/img/options-background.png'):
  2448. if not os.path.isfile(baseDir+'/accounts/options-background.png'):
  2449. copyfile(baseDir+'/img/options-background.png',baseDir+'/accounts/options-background.png')
  2450. followStr='Follow'
  2451. blockStr='Block'
  2452. if originPathStr.startswith('/users/'):
  2453. nickname=originPathStr.split('/users/')[1]
  2454. if '/' in nickname:
  2455. nickname=nickname.split('/')[0]
  2456. if '?' in nickname:
  2457. nickname=nickname.split('?')[0]
  2458. followerDomain,followerPort=getDomainFromActor(optionsActor)
  2459. if isFollowingActor(baseDir,nickname,domain,optionsActor):
  2460. followStr='Unfollow'
  2461. optionsNickname=getNicknameFromActor(optionsActor)
  2462. optionsDomainFull=optionsDomain
  2463. if optionsPort:
  2464. if optionsPort!=80 and optionsPort!=443:
  2465. optionsDomainFull=optionsDomain+':'+str(optionsPort)
  2466. if isBlocked(baseDir,nickname,domain,optionsNickname,optionsDomainFull):
  2467. blockStr='Block'
  2468. optionsLinkStr=''
  2469. if optionsLink:
  2470. optionsLinkStr=' <input type="hidden" name="postUrl" value="'+optionsLink+'">'
  2471. cssFilename=baseDir+'/epicyon-follow.css'
  2472. if os.path.isfile(baseDir+'/follow.css'):
  2473. cssFilename=baseDir+'/follow.css'
  2474. with open(cssFilename, 'r') as cssFile:
  2475. profileStyle = cssFile.read()
  2476. optionsStr=htmlHeader(cssFilename,profileStyle)
  2477. optionsStr+='<div class="options">'
  2478. optionsStr+=' <div class="optionsAvatar">'
  2479. optionsStr+=' <center>'
  2480. optionsStr+=' <a href="'+optionsActor+'">'
  2481. optionsStr+=' <img src="'+optionsProfileUrl+'"/></a>'
  2482. optionsStr+=' <p class="optionsText">'+translate['Options for']+' @'+getNicknameFromActor(optionsActor)+'@'+optionsDomain+'</p>'
  2483. optionsStr+= \
  2484. ' <form method="POST" action="'+originPathStr+'/personoptions">' \
  2485. ' <input type="hidden" name="pageNumber" value="'+str(pageNumber)+'">' \
  2486. ' <input type="hidden" name="actor" value="'+optionsActor+'">' \
  2487. ' <input type="hidden" name="avatarUrl" value="'+optionsProfileUrl+'">'+ \
  2488. optionsLinkStr+ \
  2489. ' <button type="submit" class="button" name="submitView">'+translate['View']+'</button>' \
  2490. ' <button type="submit" class="button" name="submit'+followStr+'">'+translate[followStr]+'</button>' \
  2491. ' <button type="submit" class="button" name="submit'+blockStr+'">'+translate[blockStr]+'</button>' \
  2492. ' <button type="submit" class="button" name="submitDM">'+translate['DM']+'</button>' \
  2493. ' <button type="submit" class="button" name="submitReport">'+translate['Report']+'</button>' \
  2494. ' </form>'
  2495. optionsStr+='</center>'
  2496. optionsStr+='</div>'
  2497. optionsStr+='</div>'
  2498. optionsStr+=htmlFooter()
  2499. return optionsStr
  2500. #def htmlBlockConfirm(translate: {},baseDir: str, \
  2501. # originPathStr: str, \
  2502. # blockActor: str, \
  2503. # blockProfileUrl: str) -> str:
  2504. # """Asks to confirm a block
  2505. # """
  2506. # blockDomain,port=getDomainFromActor(blockActor)
  2507. #
  2508. # if os.path.isfile(baseDir+'/img/block-background.png'):
  2509. # if not os.path.isfile(baseDir+'/accounts/block-background.png'):
  2510. # copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
  2511. #
  2512. # with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  2513. # profileStyle = cssFile.read()
  2514. # blockStr=htmlHeader(cssFilename,profileStyle)
  2515. # blockStr+='<div class="block">'
  2516. # blockStr+=' <div class="blockAvatar">'
  2517. # blockStr+=' <center>'
  2518. # blockStr+=' <a href="'+blockActor+'">'
  2519. # blockStr+=' <img src="'+blockProfileUrl+'"/></a>'
  2520. # blockStr+=' <p class="blockText">'+translate['Block']+' '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
  2521. # blockStr+= \
  2522. # ' <form method="POST" action="'+originPathStr+'/blockconfirm">' \
  2523. # ' <input type="hidden" name="actor" value="'+blockActor+'">' \
  2524. # ' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>' \
  2525. # ' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>' \
  2526. # ' </form>'
  2527. # blockStr+='</center>'
  2528. # blockStr+='</div>'
  2529. # blockStr+='</div>'
  2530. # blockStr+=htmlFooter()
  2531. # return blockStr
  2532. def htmlUnblockConfirm(translate: {},baseDir: str, \
  2533. originPathStr: str, \
  2534. blockActor: str, \
  2535. blockProfileUrl: str) -> str:
  2536. """Asks to confirm unblocking an actor
  2537. """
  2538. blockDomain,port=getDomainFromActor(blockActor)
  2539. if os.path.isfile(baseDir+'/img/block-background.png'):
  2540. if not os.path.isfile(baseDir+'/accounts/block-background.png'):
  2541. copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
  2542. cssFilename=baseDir+'/epicyon-follow.css'
  2543. if os.path.isfile(baseDir+'/follow.css'):
  2544. cssFilename=baseDir+'/follow.css'
  2545. with open(cssFilename, 'r') as cssFile:
  2546. profileStyle = cssFile.read()
  2547. blockStr=htmlHeader(cssFilename,profileStyle)
  2548. blockStr+='<div class="block">'
  2549. blockStr+=' <div class="blockAvatar">'
  2550. blockStr+=' <center>'
  2551. blockStr+=' <a href="'+blockActor+'">'
  2552. blockStr+=' <img src="'+blockProfileUrl+'"/></a>'
  2553. blockStr+=' <p class="blockText">'+translate['Stop blocking']+' '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
  2554. blockStr+= \
  2555. ' <form method="POST" action="'+originPathStr+'/unblockconfirm">' \
  2556. ' <input type="hidden" name="actor" value="'+blockActor+'">' \
  2557. ' <button type="submit" class="button" name="submitYes">'+translate['Yes']+'</button>' \
  2558. ' <a href="'+originPathStr+'"><button class="button">'+translate['No']+'</button></a>' \
  2559. ' </form>'
  2560. blockStr+='</center>'
  2561. blockStr+='</div>'
  2562. blockStr+='</div>'
  2563. blockStr+=htmlFooter()
  2564. return blockStr
  2565. def htmlSearchEmojiTextEntry(translate: {}, \
  2566. baseDir: str,path: str) -> str:
  2567. """Search for an emoji by name
  2568. """
  2569. if not os.path.isfile(baseDir+'/emoji/emoji.json'):
  2570. copyfile(baseDir+'/emoji/default_emoji.json',baseDir+'/emoji/emoji.json')
  2571. actor=path.replace('/search','')
  2572. nickname=getNicknameFromActor(actor)
  2573. domain,port=getDomainFromActor(actor)
  2574. if os.path.isfile(baseDir+'/img/search-background.png'):
  2575. if not os.path.isfile(baseDir+'/accounts/search-background.png'):
  2576. copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
  2577. cssFilename=baseDir+'/epicyon-follow.css'
  2578. if os.path.isfile(baseDir+'/follow.css'):
  2579. cssFilename=baseDir+'/follow.css'
  2580. with open(cssFilename, 'r') as cssFile:
  2581. profileStyle = cssFile.read()
  2582. emojiStr=htmlHeader(cssFilename,profileStyle)
  2583. emojiStr+='<div class="follow">'
  2584. emojiStr+=' <div class="followAvatar">'
  2585. emojiStr+=' <center>'
  2586. emojiStr+=' <p class="followText">'+translate['Enter an emoji name to search for']+'</p>'
  2587. emojiStr+= \
  2588. ' <form method="POST" action="'+actor+'/searchhandleemoji">' \
  2589. ' <input type="hidden" name="actor" value="'+actor+'">' \
  2590. ' <input type="text" name="searchtext" autofocus><br>' \
  2591. ' <button type="submit" class="button" name="submitSearch">'+translate['Submit']+'</button>' \
  2592. ' </form>'
  2593. emojiStr+=' </center>'
  2594. emojiStr+=' </div>'
  2595. emojiStr+='</div>'
  2596. emojiStr+=htmlFooter()
  2597. return emojiStr
  2598. def weekDayOfMonthStart(monthNumber: int,year: int) -> int:
  2599. """Gets the day number of the first day of the month
  2600. 1=sun, 7=sat
  2601. """
  2602. firstDayOfMonth=datetime(year, monthNumber, 1, 0, 0)
  2603. return int(firstDayOfMonth.strftime("%w"))+1
  2604. def getCalendarEvents(baseDir: str,nickname: str,domain: str,year: int,monthNumber: int) -> {}:
  2605. """Retrieves calendar events
  2606. Returns a dictionary indexed by day number of lists containing Event and Place activities
  2607. """
  2608. calendarFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/calendar/'+str(year)+'/'+str(monthNumber)+'.txt'
  2609. events={}
  2610. if not os.path.isfile(calendarFilename):
  2611. return events
  2612. calendarPostIds=[]
  2613. recreateEventsFile=False
  2614. with open(calendarFilename,'r') as eventsFile:
  2615. for postId in eventsFile:
  2616. postId=postId.replace('\n','')
  2617. postFilename=locatePost(baseDir,nickname,domain,postId)
  2618. if postFilename:
  2619. postJsonObject=None
  2620. tries=0
  2621. postJsonObject=None
  2622. while tries<5:
  2623. try:
  2624. with open(postFilename, 'r') as fp:
  2625. postJsonObject=commentjson.load(fp)
  2626. break
  2627. except Exception as e:
  2628. print(e)
  2629. time.sleep(1)
  2630. tries+=1
  2631. if postJsonObject:
  2632. if postJsonObject.get('object'):
  2633. if isinstance(postJsonObject['object'], dict):
  2634. if postJsonObject['object'].get('tag'):
  2635. postEvent=[]
  2636. dayOfMonth=None
  2637. for tag in postJsonObject['object']['tag']:
  2638. if not tag.get('type'):
  2639. continue
  2640. if tag['type']!='Event' and tag['type']!='Place':
  2641. continue
  2642. if tag['type']=='Event':
  2643. # tag is an event
  2644. if not tag.get('startTime'):
  2645. continue
  2646. eventTime= \
  2647. datetime.strptime(tag['startTime'], \
  2648. "%Y-%m-%dT%H:%M:%S%z")
  2649. if int(eventTime.strftime("%Y"))==year and \
  2650. int(eventTime.strftime("%m"))==monthNumber:
  2651. dayOfMonth=str(int(eventTime.strftime("%d")))
  2652. postEvent.append(tag)
  2653. else:
  2654. # tag is a place
  2655. postEvent.append(tag)
  2656. if postEvent and dayOfMonth:
  2657. calendarPostIds.append(postId)
  2658. if not events.get(dayOfMonth):
  2659. events[dayOfMonth]=[]
  2660. events[dayOfMonth].append(postEvent)
  2661. else:
  2662. recreateEventsFile=True
  2663. # if some posts have been deleted then regenerate the calendar file
  2664. if recreateEventsFile:
  2665. calendarFile=open(calendarFilename, "w")
  2666. for postId in calendarPostIds:
  2667. calendarFile.write(postId+'\n')
  2668. calendarFile.close()
  2669. return events
  2670. def htmlCalendarDay(translate: {}, \
  2671. baseDir: str,path: str, \
  2672. year: int,monthNumber: int,dayNumber: int,
  2673. nickname: str,domain: str,dayEvents: [], \
  2674. monthName: str, actor: str) -> str:
  2675. """Show a day within the calendar
  2676. """
  2677. accountDir=baseDir+'/accounts/'+nickname+'@'+domain
  2678. calendarFile=accountDir+'/.newCalendar'
  2679. if os.path.isfile(calendarFile):
  2680. os.remove(calendarFile)
  2681. cssFilename=baseDir+'/epicyon-calendar.css'
  2682. if os.path.isfile(baseDir+'/calendar.css'):
  2683. cssFilename=baseDir+'/calendar.css'
  2684. with open(cssFilename, 'r') as cssFile:
  2685. calendarStyle = cssFile.read()
  2686. calendarStr=htmlHeader(cssFilename,calendarStyle)
  2687. calendarStr+='<main><table class="calendar">\n'
  2688. calendarStr+='<caption class="calendar__banner--month">\n'
  2689. calendarStr+=' <a href="'+actor+'/calendar?year='+str(year)+'?month='+str(monthNumber)+'">'
  2690. calendarStr+=' <h1>'+str(dayNumber)+' '+monthName+'</h1></a><br><span class="year">'+str(year)+'</span>\n'
  2691. calendarStr+='</caption>\n'
  2692. calendarStr+='<tbody>\n'
  2693. for eventPost in dayEvents:
  2694. eventTime=None
  2695. eventDescription=None
  2696. eventPlace=None
  2697. for ev in eventPost:
  2698. if ev['type']=='Event':
  2699. if ev.get('startTime'):
  2700. eventDate=datetime.strptime(ev['startTime'],"%Y-%m-%dT%H:%M:%S%z")
  2701. eventTime=eventDate.strftime("%H:%M").strip()
  2702. if ev.get('name'):
  2703. eventDescription=ev['name'].strip()
  2704. elif ev['type']=='Place':
  2705. if ev.get('name'):
  2706. eventPlace=ev['name']
  2707. if eventTime and eventDescription and eventPlace:
  2708. 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'
  2709. elif eventTime and eventDescription and not eventPlace:
  2710. calendarStr+='<tr><td class="calendar__day__time"><b>'+eventTime+'</b></td><td class="calendar__day__event">'+eventDescription+'</td></tr>\n'
  2711. elif not eventTime and eventDescription and not eventPlace:
  2712. calendarStr+='<tr><td class="calendar__day__time"></td><td class="calendar__day__event">'+eventDescription+'</td></tr>\n'
  2713. elif not eventTime and eventDescription and eventPlace:
  2714. calendarStr+='<tr><td class="calendar__day__time"></td><td class="calendar__day__event"><span class="place">'+eventPlace+'</span><br>'+eventDescription+'</td></tr>\n'
  2715. elif eventTime and not eventDescription and eventPlace:
  2716. calendarStr+='<tr><td class="calendar__day__time"><b>'+eventTime+'</b></td><td class="calendar__day__event"><span class="place">'+eventPlace+'</span></td></tr>\n'
  2717. calendarStr+='</tbody>\n'
  2718. calendarStr+='</table></main>\n'
  2719. calendarStr+=htmlFooter()
  2720. return calendarStr
  2721. def htmlCalendar(translate: {}, \
  2722. baseDir: str,path: str, \
  2723. httpPrefix: str,domainFull: str) -> str:
  2724. """Show the calendar for a person
  2725. """
  2726. iconsDir=getIconsDir(baseDir)
  2727. domain=domainFull
  2728. if ':' in domainFull:
  2729. domain=domainFull.split(':')[0]
  2730. monthNumber=0
  2731. dayNumber=None
  2732. year=1970
  2733. actor=httpPrefix+'://'+domainFull+path.replace('/calendar','')
  2734. if '?' in actor:
  2735. first=True
  2736. for p in actor.split('?'):
  2737. if not first:
  2738. if '=' in p:
  2739. if p.split('=')[0]=='year':
  2740. numStr=p.split('=')[1]
  2741. if numStr.isdigit():
  2742. year=int(numStr)
  2743. elif p.split('=')[0]=='month':
  2744. numStr=p.split('=')[1]
  2745. if numStr.isdigit():
  2746. monthNumber=int(numStr)
  2747. elif p.split('=')[0]=='day':
  2748. numStr=p.split('=')[1]
  2749. if numStr.isdigit():
  2750. dayNumber=int(numStr)
  2751. first=False
  2752. actor=actor.split('?')[0]
  2753. currDate=datetime.now()
  2754. if year==1970 and monthNumber==0:
  2755. year=currDate.year
  2756. monthNumber=currDate.month
  2757. nickname=getNicknameFromActor(actor)
  2758. events=getCalendarEvents(baseDir,nickname,domain,year,monthNumber)
  2759. months=['Jaruary','February','March','April','May','June','July','August','September','October','November','December']
  2760. monthName=translate[months[monthNumber-1]]
  2761. if os.path.isfile(baseDir+'/img/calendar-background.png'):
  2762. if not os.path.isfile(baseDir+'/accounts/calendar-background.png'):
  2763. copyfile(baseDir+'/img/calendar-background.png',baseDir+'/accounts/calendar-background.png')
  2764. if dayNumber:
  2765. dayEvents=None
  2766. if events.get(str(dayNumber)):
  2767. dayEvents=events[str(dayNumber)]
  2768. return htmlCalendarDay(translate,baseDir,path, \
  2769. year,monthNumber,dayNumber, \
  2770. nickname,domain,dayEvents, \
  2771. monthName,actor)
  2772. prevYear=year
  2773. prevMonthNumber=monthNumber-1
  2774. if prevMonthNumber<1:
  2775. prevMonthNumber=12
  2776. prevYear=year-1
  2777. nextYear=year
  2778. nextMonthNumber=monthNumber+1
  2779. if nextMonthNumber>12:
  2780. nextMonthNumber=1
  2781. nextYear=year+1
  2782. print('Calendar year='+str(year)+' month='+str(monthNumber)+ ' '+str(weekDayOfMonthStart(monthNumber,year)))
  2783. if monthNumber<12:
  2784. daysInMonth=(date(year, monthNumber+1, 1) - date(year, monthNumber, 1)).days
  2785. else:
  2786. daysInMonth=(date(year+1, 1, 1) - date(year, monthNumber, 1)).days
  2787. cssFilename=baseDir+'/epicyon-calendar.css'
  2788. if os.path.isfile(baseDir+'/calendar.css'):
  2789. cssFilename=baseDir+'/calendar.css'
  2790. with open(cssFilename, 'r') as cssFile:
  2791. calendarStyle = cssFile.read()
  2792. calendarStr=htmlHeader(cssFilename,calendarStyle)
  2793. calendarStr+='<main><table class="calendar">\n'
  2794. calendarStr+='<caption class="calendar__banner--month">\n'
  2795. calendarStr+=' <a href="'+actor+'/calendar?year='+str(prevYear)+'?month='+str(prevMonthNumber)+'">'
  2796. calendarStr+=' <img src="/'+iconsDir+'/prev.png" class="buttonprev"/></a>\n'
  2797. calendarStr+=' <a href="'+actor+'/inbox">'
  2798. calendarStr+=' <h1>'+monthName+'</h1></a>\n'
  2799. calendarStr+=' <a href="'+actor+'/calendar?year='+str(nextYear)+'?month='+str(nextMonthNumber)+'">'
  2800. calendarStr+=' <img src="/'+iconsDir+'/prev.png" class="buttonnext"/></a>\n'
  2801. calendarStr+='</caption>\n'
  2802. calendarStr+='<thead>\n'
  2803. calendarStr+='<tr>\n'
  2804. calendarStr+=' <th class="calendar__day__header">'+translate['Sun']+'</th>\n'
  2805. calendarStr+=' <th class="calendar__day__header">'+translate['Mon']+'</th>\n'
  2806. calendarStr+=' <th class="calendar__day__header">'+translate['Tue']+'</th>\n'
  2807. calendarStr+=' <th class="calendar__day__header">'+translate['Wed']+'</th>\n'
  2808. calendarStr+=' <th class="calendar__day__header">'+translate['Thu']+'</th>\n'
  2809. calendarStr+=' <th class="calendar__day__header">'+translate['Fri']+'</th>\n'
  2810. calendarStr+=' <th class="calendar__day__header">'+translate['Sat']+'</th>\n'
  2811. calendarStr+='</tr>\n'
  2812. calendarStr+='</thead>\n'
  2813. calendarStr+='<tbody>\n'
  2814. dayOfMonth=0
  2815. dow=weekDayOfMonthStart(monthNumber,year)
  2816. for weekOfMonth in range(1,6):
  2817. calendarStr+=' <tr>\n'
  2818. for dayNumber in range(1,8):
  2819. if (weekOfMonth>1 and dayOfMonth<daysInMonth) or \
  2820. (weekOfMonth==1 and dayNumber>=dow):
  2821. dayOfMonth+=1
  2822. isToday=False
  2823. if year==currDate.year:
  2824. if currDate.month==monthNumber:
  2825. if dayOfMonth==currDate.day:
  2826. isToday=True
  2827. if events.get(str(dayOfMonth)):
  2828. url=actor+'/calendar?year='+str(year)+'?month='+str(monthNumber)+'?day='+str(dayOfMonth)
  2829. dayLink='<a href="'+url+'">'+str(dayOfMonth)+'</a>'
  2830. # there are events for this day
  2831. if not isToday:
  2832. calendarStr+=' <td class="calendar__day__cell" data-event="">'+dayLink+'</td>\n'
  2833. else:
  2834. calendarStr+=' <td class="calendar__day__cell" data-today-event="">'+dayLink+'</td>\n'
  2835. else:
  2836. # No events today
  2837. if not isToday:
  2838. calendarStr+=' <td class="calendar__day__cell">'+str(dayOfMonth)+'</td>\n'
  2839. else:
  2840. calendarStr+=' <td class="calendar__day__cell" data-today="">'+str(dayOfMonth)+'</td>\n'
  2841. else:
  2842. calendarStr+=' <td class="calendar__day__cell"></td>\n'
  2843. calendarStr+=' </tr>\n'
  2844. calendarStr+='</tbody>\n'
  2845. calendarStr+='</table></main>\n'
  2846. calendarStr+=htmlFooter()
  2847. return calendarStr
  2848. def htmlSearch(translate: {}, \
  2849. baseDir: str,path: str) -> str:
  2850. """Search called from the timeline icon
  2851. """
  2852. actor=path.replace('/search','')
  2853. nickname=getNicknameFromActor(actor)
  2854. domain,port=getDomainFromActor(actor)
  2855. if os.path.isfile(baseDir+'/img/search-background.png'):
  2856. if not os.path.isfile(baseDir+'/accounts/search-background.png'):
  2857. copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
  2858. cssFilename=baseDir+'/epicyon-follow.css'
  2859. if os.path.isfile(baseDir+'/follow.css'):
  2860. cssFilename=baseDir+'/follow.css'
  2861. with open(cssFilename, 'r') as cssFile:
  2862. profileStyle = cssFile.read()
  2863. followStr=htmlHeader(cssFilename,profileStyle)
  2864. followStr+='<div class="follow">'
  2865. followStr+=' <div class="followAvatar">'
  2866. followStr+=' <center>'
  2867. followStr+=' <p class="followText">'+translate['Enter an address, shared item, #hashtag, *skill or :emoji: to search for']+'</p>'
  2868. followStr+= \
  2869. ' <form method="POST" action="'+actor+'/searchhandle">' \
  2870. ' <input type="hidden" name="actor" value="'+actor+'">' \
  2871. ' <input type="text" name="searchtext" autofocus><br>' \
  2872. ' <button type="submit" class="button" name="submitSearch">'+translate['Submit']+'</button>' \
  2873. ' </form>'
  2874. followStr+=' </center>'
  2875. followStr+=' </div>'
  2876. followStr+='</div>'
  2877. followStr+=htmlFooter()
  2878. return followStr
  2879. def htmlProfileAfterSearch(translate: {}, \
  2880. baseDir: str,path: str,httpPrefix: str, \
  2881. nickname: str,domain: str,port: int, \
  2882. profileHandle: str, \
  2883. session,wfRequest: {},personCache: {},
  2884. debug: bool,projectVersion: str) -> str:
  2885. """Show a profile page after a search for a fediverse address
  2886. """
  2887. if '/users/' in profileHandle or '/@' in profileHandle:
  2888. searchNickname=getNicknameFromActor(profileHandle)
  2889. searchDomain,searchPort=getDomainFromActor(profileHandle)
  2890. else:
  2891. if '@' not in profileHandle:
  2892. if debug:
  2893. print('DEBUG: no @ in '+profileHandle)
  2894. return None
  2895. if profileHandle.startswith('@'):
  2896. profileHandle=profileHandle[1:]
  2897. if '@' not in profileHandle:
  2898. if debug:
  2899. print('DEBUG: no @ in '+profileHandle)
  2900. return None
  2901. searchNickname=profileHandle.split('@')[0]
  2902. searchDomain=profileHandle.split('@')[1]
  2903. searchPort=None
  2904. if ':' in searchDomain:
  2905. searchPort=int(searchDomain.split(':')[1])
  2906. searchDomain=searchDomain.split(':')[0]
  2907. if not searchNickname:
  2908. if debug:
  2909. print('DEBUG: No nickname found in '+profileHandle)
  2910. return None
  2911. if not searchDomain:
  2912. if debug:
  2913. print('DEBUG: No domain found in '+profileHandle)
  2914. return None
  2915. searchDomainFull=searchDomain
  2916. if searchPort:
  2917. if searchPort!=80 and searchPort!=443:
  2918. if ':' not in searchDomain:
  2919. searchDomainFull=searchDomain+':'+str(searchPort)
  2920. profileStr=''
  2921. cssFilename=baseDir+'/epicyon-profile.css'
  2922. if os.path.isfile(baseDir+'/epicyon.css'):
  2923. cssFilename=baseDir+'/epicyon.css'
  2924. with open(cssFilename, 'r') as cssFile:
  2925. wf = webfingerHandle(session,searchNickname+'@'+searchDomainFull,httpPrefix,wfRequest, \
  2926. domain,projectVersion)
  2927. if not wf:
  2928. if debug:
  2929. print('DEBUG: Unable to webfinger '+searchNickname+'@'+searchDomainFull)
  2930. return None
  2931. asHeader = {'Accept': 'application/activity+json; profile="https://www.w3.org/ns/activitystreams"'}
  2932. personUrl = getUserUrl(wf)
  2933. if not personUrl:
  2934. if debug:
  2935. print('DEBUG: Webfinger did not return an actor url')
  2936. return None
  2937. profileJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
  2938. if not profileJson:
  2939. if debug:
  2940. print('DEBUG: No actor returned from '+personUrl)
  2941. return None
  2942. avatarUrl=''
  2943. if profileJson.get('icon'):
  2944. if profileJson['icon'].get('url'):
  2945. avatarUrl=profileJson['icon']['url']
  2946. if not avatarUrl:
  2947. avatarUrl=getPersonAvatarUrl(baseDir,personUrl,personCache)
  2948. displayName=searchNickname
  2949. if profileJson.get('name'):
  2950. displayName=profileJson['name']
  2951. profileDescription=''
  2952. if profileJson.get('summary'):
  2953. profileDescription=profileJson['summary']
  2954. outboxUrl=None
  2955. if not profileJson.get('outbox'):
  2956. if debug:
  2957. pprint(profileJson)
  2958. print('DEBUG: No outbox found')
  2959. return None
  2960. outboxUrl=profileJson['outbox']
  2961. profileBackgroundImage=''
  2962. if profileJson.get('image'):
  2963. if profileJson['image'].get('url'):
  2964. profileBackgroundImage=profileJson['image']['url']
  2965. profileStyle = cssFile.read().replace('image.png',profileBackgroundImage)
  2966. # url to return to
  2967. backUrl=path
  2968. if not backUrl.endswith('/inbox'):
  2969. backUrl+='/inbox'
  2970. profileStr= \
  2971. ' <div class="hero-image">' \
  2972. ' <div class="hero-text">' \
  2973. ' <img src="'+avatarUrl+'" alt="'+searchNickname+'@'+searchDomainFull+'">' \
  2974. ' <h1>'+displayName+'</h1>' \
  2975. ' <p><b>@'+searchNickname+'@'+searchDomainFull+'</b></p>' \
  2976. ' <p>'+profileDescription+'</p>'+ \
  2977. ' </div>' \
  2978. '</div>'+ \
  2979. '<div class="container">\n' \
  2980. ' <form method="POST" action="'+backUrl+'/followconfirm">' \
  2981. ' <center>' \
  2982. ' <input type="hidden" name="actor" value="'+personUrl+'">' \
  2983. ' <button type="submit" class="button" name="submitYes">'+translate['Follow']+'</button>' \
  2984. ' <a href="'+backUrl+'"><button class="button">'+translate['Go Back']+'</button></a>' \
  2985. ' </center>' \
  2986. ' </form>' \
  2987. '</div>'
  2988. profileStr+='<script>'+contentWarningScript()+'</script>'
  2989. iconsDir=getIconsDir(baseDir)
  2990. result = []
  2991. i = 0
  2992. for item in parseUserFeed(session,outboxUrl,asHeader, \
  2993. projectVersion,httpPrefix,domain):
  2994. if not item.get('type'):
  2995. continue
  2996. if item['type']!='Create' and item['type']!='Announce':
  2997. continue
  2998. if not item.get('object'):
  2999. continue
  3000. profileStr+= \
  3001. individualPostAsHtml(iconsDir,translate,None,baseDir, \
  3002. session,wfRequest,personCache, \
  3003. nickname,domain,port, \
  3004. item,avatarUrl,False,False, \
  3005. httpPrefix,projectVersion,'inbox', \
  3006. False,False,False,False)
  3007. i+=1
  3008. if i>=20:
  3009. break
  3010. return htmlHeader(cssFilename,profileStyle)+profileStr+htmlFooter()