webinterface.py 120 KB

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