webinterface.py 128 KB

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