webinterface.py 74 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635
  1. __filename__ = "webinterface.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "0.0.1"
  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 shutil import copyfile
  14. from pprint import pprint
  15. from person import personBoxJson
  16. from utils import getNicknameFromActor
  17. from utils import getDomainFromActor
  18. from utils import locatePost
  19. from utils import noOfAccounts
  20. from utils import isPublicPost
  21. from follow import isFollowingActor
  22. from webfinger import webfingerHandle
  23. from posts import getPersonBox
  24. from posts import getUserUrl
  25. from posts import parseUserFeed
  26. from posts import populateRepliesJson
  27. from posts import isModerator
  28. from session import getJson
  29. from auth import createPassword
  30. from like import likedByPerson
  31. from announce import announcedByPerson
  32. from blocking import isBlocked
  33. from content import getMentionsFromHtml
  34. from config import getConfigParam
  35. from skills import getSkills
  36. from cache import getPersonFromCache
  37. def getPersonAvatarUrl(personUrl: str,personCache: {}) -> str:
  38. """Returns the avatar url for the person
  39. """
  40. personJson = getPersonFromCache(personUrl,personCache)
  41. if personJson:
  42. if personJson.get('icon'):
  43. if personJson['icon'].get('url'):
  44. return personJson['icon']['url']
  45. return None
  46. def htmlSearchSharedItems(baseDir: str,searchStr: str,pageNumber: int,resultsPerPage: int,actor: str) -> str:
  47. """Search results for shared items
  48. """
  49. currPage=1
  50. ctr=0
  51. sharedItemsForm=''
  52. searchStrLower=searchStr.replace('%2B','+').replace('%40','@').replace('%3A',':').replace('%23','#').lower().strip('\n')
  53. searchStrLowerList=searchStrLower.split('+')
  54. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  55. sharedItemsCSS=cssFile.read()
  56. sharedItemsForm=htmlHeader(sharedItemsCSS)
  57. sharedItemsForm+='<center><h1>Shared Items Search</h1></center>'
  58. resultsExist=False
  59. for subdir, dirs, files in os.walk(baseDir+'/accounts'):
  60. for handle in dirs:
  61. if '@' not in handle:
  62. continue
  63. sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
  64. if not os.path.isfile(sharesFilename):
  65. continue
  66. with open(sharesFilename, 'r') as fp:
  67. sharesJson=commentjson.load(fp)
  68. for name,sharedItem in sharesJson.items():
  69. matched=True
  70. for searchSubstr in searchStrLowerList:
  71. subStrMatched=False
  72. searchSubstr=searchSubstr.strip()
  73. if searchSubstr in sharedItem['location'].lower():
  74. subStrMatched=True
  75. elif searchSubstr in sharedItem['summary'].lower():
  76. subStrMatched=True
  77. elif searchSubstr in sharedItem['displayName'].lower():
  78. subStrMatched=True
  79. elif searchSubstr in sharedItem['category'].lower():
  80. subStrMatched=True
  81. if not subStrMatched:
  82. matched=False
  83. break
  84. if matched:
  85. if currPage==pageNumber:
  86. sharedItemsForm+='<div class="container">'
  87. sharedItemsForm+='<p class="share-title">'+sharedItem['displayName']+'</p>'
  88. sharedItemsForm+='<a href="'+sharedItem['imageUrl']+'">'
  89. sharedItemsForm+='<img src="'+sharedItem['imageUrl']+'" alt="Item image"></a>'
  90. sharedItemsForm+='<p>'+sharedItem['summary']+'</p>'
  91. sharedItemsForm+='<p><b>Type:</b> '+sharedItem['itemType']+' '
  92. sharedItemsForm+='<b>Category:</b> '+sharedItem['category']+' '
  93. sharedItemsForm+='<b>Location:</b> '+sharedItem['location']+'</p>'
  94. sharedItemsForm+='</div>'
  95. if not resultsExist and currPage>1:
  96. # previous page link, needs to be a POST
  97. sharedItemsForm+= \
  98. '<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber-1)+'">' \
  99. ' <input type="hidden" name="actor" value="'+actor+'">' \
  100. ' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>' \
  101. ' <center><a href="'+actor+'" type="submit" name="submitSearch">' \
  102. ' <img class="pageicon" src="/icons/pageup.png" title="Page up" alt="Page up"/></a>' \
  103. ' </center>' \
  104. '</form>'
  105. resultsExist=True
  106. ctr+=1
  107. if ctr>=resultsPerPage:
  108. currPage+=1
  109. if currPage>pageNumber:
  110. # next page link, needs to be a POST
  111. sharedItemsForm+= \
  112. '<form method="POST" action="'+actor+'/searchhandle?page='+str(pageNumber+1)+'">' \
  113. ' <input type="hidden" name="actor" value="'+actor+'">' \
  114. ' <input type="hidden" name="searchtext" value="'+searchStrLower+'"><br>' \
  115. ' <center><a href="'+actor+'" type="submit" name="submitSearch">' \
  116. ' <img class="pageicon" src="/icons/pagedown.png" title="Page down" alt="Page down"/></a>' \
  117. ' </center>' \
  118. '</form>'
  119. break
  120. ctr=0
  121. if not resultsExist:
  122. sharedItemsForm+='<center><h5>No results</h5></center>'
  123. sharedItemsForm+=htmlFooter()
  124. return sharedItemsForm
  125. def htmlModerationInfo(baseDir: str) -> str:
  126. infoForm=''
  127. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  128. infoCSS=cssFile.read()
  129. infoForm=htmlHeader(infoCSS)
  130. infoForm+='<center><h1>Moderation Information</h1></center>'
  131. infoShown=False
  132. suspendedFilename=baseDir+'/accounts/suspended.txt'
  133. if os.path.isfile(suspendedFilename):
  134. with open(suspendedFilename, "r") as f:
  135. suspendedStr = f.read()
  136. infoForm+= \
  137. '<div class="container">' \
  138. ' <br><b>Suspended accounts</b>' \
  139. ' <br>These are currently suspended' \
  140. ' <textarea id="message" name="suspended" style="height:200px">'+suspendedStr+'</textarea>' \
  141. '</div>'
  142. infoShown=True
  143. blockingFilename=baseDir+'/accounts/blocking.txt'
  144. if os.path.isfile(blockingFilename):
  145. with open(blockingFilename, "r") as f:
  146. blockedStr = f.read()
  147. infoForm+= \
  148. '<div class="container">' \
  149. ' <br><b>Blocked accounts and hashtags</b>' \
  150. ' <br>These are globally blocked for all accounts on this instance' \
  151. ' <textarea id="message" name="blocked" style="height:200px">'+blockedStr+'</textarea>' \
  152. '</div>'
  153. infoShown=True
  154. if not infoShown:
  155. infoForm+='<center><p>Any blocks or suspensions made by moderators will be shown here.</p></center>'
  156. infoForm+=htmlFooter()
  157. return infoForm
  158. def htmlHashtagSearch(baseDir: str,hashtag: str,pageNumber: int,postsPerPage: int,
  159. session,wfRequest: {},personCache: {}, \
  160. httpPrefix: str,projectVersion: str) -> str:
  161. """Show a page containing search results for a hashtag
  162. """
  163. if hashtag.startswith('#'):
  164. hashtag=hashtag[1:]
  165. hashtagIndexFile=baseDir+'/tags/'+hashtag+'.txt'
  166. if not os.path.isfile(hashtagIndexFile):
  167. return None
  168. # read the index
  169. with open(hashtagIndexFile, "r") as f:
  170. lines = f.readlines()
  171. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  172. hashtagSearchCSS = cssFile.read()
  173. startIndex=len(lines)-1-int(pageNumber*postsPerPage)
  174. if startIndex<0:
  175. startIndex=len(lines)-1
  176. endIndex=startIndex-postsPerPage
  177. if endIndex<0:
  178. endIndex=0
  179. hashtagSearchForm=htmlHeader(hashtagSearchCSS)
  180. hashtagSearchForm+='<center><h1>#'+hashtag+'</h1></center>'
  181. if startIndex!=len(lines)-1:
  182. # previous page link
  183. hashtagSearchForm+='<center><a href="/tags/'+hashtag+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/icons/pageup.png" title="Page up" alt="Page up"></a></center>'
  184. index=startIndex
  185. while index>=endIndex:
  186. postId=lines[index].strip('\n')
  187. nickname=getNicknameFromActor(postId)
  188. if not nickname:
  189. index-=1
  190. continue
  191. domain,port=getDomainFromActor(postId)
  192. if not domain:
  193. index-=1
  194. continue
  195. postFilename=locatePost(baseDir,nickname,domain,postId)
  196. if not postFilename:
  197. index-=1
  198. continue
  199. with open(postFilename, 'r') as fp:
  200. postJsonObject=commentjson.load(fp)
  201. if not isPublicPost(postJsonObject):
  202. index-=1
  203. continue
  204. hashtagSearchForm+= \
  205. individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  206. nickname,domain,port,postJsonObject, \
  207. None,True,False, \
  208. httpPrefix,projectVersion, \
  209. False)
  210. index-=1
  211. if endIndex>0:
  212. # next page link
  213. hashtagSearchForm+='<center><a href="/tags/'+hashtag+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/icons/pagedown.png" title="Page down" alt="Page down"></a></center>'
  214. hashtagSearchForm+=htmlFooter()
  215. return hashtagSearchForm
  216. def htmlEditProfile(baseDir: str,path: str,domain: str,port: int) -> str:
  217. """Shows the edit profile screen
  218. """
  219. pathOriginal=path
  220. path=path.replace('/inbox','').replace('/outbox','').replace('/shares','')
  221. nickname=getNicknameFromActor(path)
  222. domainFull=domain
  223. if port:
  224. if port!=80 and port!=443:
  225. if ':' not in domain:
  226. domainFull=domain+':'+str(port)
  227. actorFilename=baseDir+'/accounts/'+nickname+'@'+domain+'.json'
  228. if not os.path.isfile(actorFilename):
  229. return ''
  230. isBot=''
  231. preferredNickname=nickname
  232. bioStr=''
  233. manuallyApprovesFollowers=''
  234. with open(actorFilename, 'r') as fp:
  235. actorJson=commentjson.load(fp)
  236. if actorJson.get('preferredUsername'):
  237. preferredNickname=actorJson['preferredUsername']
  238. if actorJson.get('summary'):
  239. bioStr=actorJson['summary']
  240. if actorJson.get('manuallyApprovesFollowers'):
  241. if actorJson['manuallyApprovesFollowers']:
  242. manuallyApprovesFollowers='checked'
  243. else:
  244. manuallyApprovesFollowers=''
  245. if actorJson.get('type'):
  246. if actorJson['type']=='Service':
  247. isBot='checked'
  248. filterStr=''
  249. filterFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/filters.txt'
  250. if os.path.isfile(filterFilename):
  251. with open(filterFilename, 'r') as filterfile:
  252. filterStr=filterfile.read()
  253. blockedStr=''
  254. blockedFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/blocking.txt'
  255. if os.path.isfile(blockedFilename):
  256. with open(blockedFilename, 'r') as blockedfile:
  257. blockedStr=blockedfile.read()
  258. allowedInstancesStr=''
  259. allowedInstancesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/allowedinstances.txt'
  260. if os.path.isfile(allowedInstancesFilename):
  261. with open(allowedInstancesFilename, 'r') as allowedInstancesFile:
  262. allowedInstancesStr=allowedInstancesFile.read()
  263. skills=getSkills(baseDir,nickname,domain)
  264. skillsStr=''
  265. skillCtr=1
  266. if skills:
  267. for skillDesc,skillValue in skills.items():
  268. skillsStr+='<p><input type="text" placeholder="Skill '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="'+skillDesc+'" style="width:40%">'
  269. skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="'+str(skillValue)+'"></p>'
  270. skillCtr+=1
  271. skillsStr+='<p><input type="text" placeholder="Skill '+str(skillCtr)+'" name="skillName'+str(skillCtr)+'" value="" style="width:40%">'
  272. skillsStr+='<input type="range" min="1" max="100" class="slider" name="skillValue'+str(skillCtr)+'" value="50"></p>' \
  273. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  274. editProfileCSS = cssFile.read()
  275. moderatorsStr=''
  276. adminNickname=getConfigParam(baseDir,'admin')
  277. if path.startswith('/users/'+adminNickname+'/'):
  278. moderators=''
  279. moderatorsFile=baseDir+'/accounts/moderators.txt'
  280. if os.path.isfile(moderatorsFile):
  281. with open(moderatorsFile, "r") as f:
  282. moderators = f.read()
  283. moderatorsStr= \
  284. '<div class="container">' \
  285. ' <b>Moderators</b><br>' \
  286. ' A list of moderator nicknames. One per line.' \
  287. ' <textarea id="message" name="moderators" placeholder="List of moderator nicknames..." style="height:200px">'+moderators+'</textarea>' \
  288. '</div>'
  289. editProfileForm=htmlHeader(editProfileCSS)
  290. editProfileForm+= \
  291. '<form enctype="multipart/form-data" method="POST" action="'+path+'/profiledata">' \
  292. ' <div class="vertical-center">' \
  293. ' <p class="new-post-text">Profile for '+nickname+'@'+domainFull+'</p>' \
  294. ' <div class="container">' \
  295. ' <input type="submit" name="submitProfile" value="Submit">' \
  296. ' <a href="'+pathOriginal+'"><button class="cancelbtn">Cancel</button></a>' \
  297. ' </div>'+ \
  298. ' <div class="container">' \
  299. ' <input type="text" placeholder="Preferred name" name="preferredNickname" value="'+preferredNickname+'">' \
  300. ' <textarea id="message" name="bio" placeholder="Your bio..." style="height:200px">'+bioStr+'</textarea>' \
  301. ' </div>' \
  302. ' <div class="container">' \
  303. ' Avatar image' \
  304. ' <input type="file" id="avatar" name="avatar"' \
  305. ' accept=".png">' \
  306. ' <br>Background image' \
  307. ' <input type="file" id="image" name="image"' \
  308. ' accept=".png">' \
  309. ' <br>Timeline banner image' \
  310. ' <input type="file" id="banner" name="banner"' \
  311. ' accept=".png">' \
  312. ' </div>' \
  313. ' <div class="container">' \
  314. ' <input type="checkbox" class=profilecheckbox" name="approveFollowers" '+manuallyApprovesFollowers+'>Approve follower requests<br>' \
  315. ' <input type="checkbox" class=profilecheckbox" name="isBot" '+isBot+'>This is a bot account<br>' \
  316. ' <br><b>Filtered words</b>' \
  317. ' <br>One per line' \
  318. ' <textarea id="message" name="filteredWords" placeholder="" style="height:200px">'+filterStr+'</textarea>' \
  319. ' <br><b>Blocked accounts</b>' \
  320. ' <br>Blocked accounts, one per line, in the form <i>nickname@domain</i> or <i>*@blockeddomain</i>' \
  321. ' <textarea id="message" name="blocked" placeholder="" style="height:200px">'+blockedStr+'</textarea>' \
  322. ' <br><b>Federation list</b>' \
  323. ' <br>Federate only with a defined set of instances. One domain name per line.' \
  324. ' <textarea id="message" name="allowedInstances" placeholder="" style="height:200px">'+allowedInstancesStr+'</textarea>' \
  325. ' </div>' \
  326. ' <div class="container">' \
  327. ' <b>Skills</b><br>' \
  328. ' 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.'+ \
  329. skillsStr+moderatorsStr+ \
  330. ' </div>' \
  331. ' </div>' \
  332. '</form>'
  333. editProfileForm+=htmlFooter()
  334. return editProfileForm
  335. def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str,bool):
  336. """Receives login credentials via HTTPServer POST
  337. """
  338. if not loginParams.startswith('username='):
  339. return None,None,None
  340. # minimum time between login attempts
  341. currTime=int(time.time())
  342. if currTime<lastLoginTime+10:
  343. return None,None,None
  344. if '&' not in loginParams:
  345. return None,None,None
  346. loginArgs=loginParams.split('&')
  347. nickname=None
  348. password=None
  349. register=False
  350. for arg in loginArgs:
  351. if '=' in arg:
  352. if arg.split('=',1)[0]=='username':
  353. nickname=arg.split('=',1)[1]
  354. elif arg.split('=',1)[0]=='password':
  355. password=arg.split('=',1)[1]
  356. elif arg.split('=',1)[0]=='register':
  357. register=True
  358. return nickname,password,register
  359. def htmlLogin(baseDir: str) -> str:
  360. """Shows the login screen
  361. """
  362. accounts=noOfAccounts(baseDir)
  363. if not os.path.isfile(baseDir+'/accounts/login.png'):
  364. copyfile(baseDir+'/img/login.png',baseDir+'/accounts/login.png')
  365. if os.path.isfile(baseDir+'/img/login-background.png'):
  366. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  367. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  368. if accounts>0:
  369. loginText='<p class="login-text">Welcome. Please enter your login details below.</p>'
  370. else:
  371. loginText='<p class="login-text">Please enter some credentials</p><p>You will become the admin of this site.</p>'
  372. if os.path.isfile(baseDir+'/accounts/login.txt'):
  373. # custom login message
  374. with open(baseDir+'/accounts/login.txt', 'r') as file:
  375. loginText = '<p class="login-text">'+file.read()+'</p>'
  376. with open(baseDir+'/epicyon-login.css', 'r') as cssFile:
  377. loginCSS = cssFile.read()
  378. # show the register button
  379. registerButtonStr=''
  380. if getConfigParam(baseDir,'registration')=='open':
  381. if int(getConfigParam(baseDir,'registrationsRemaining'))>0:
  382. if accounts>0:
  383. loginText='<p class="login-text">Welcome. Please login or register a new account.</p>'
  384. registerButtonStr='<button type="submit" name="register">Register</button>'
  385. TOSstr='<p class="login-text"><a href="/terms">Terms of Service</a></p>'
  386. loginButtonStr=''
  387. if accounts>0:
  388. loginButtonStr='<button type="submit" name="submit">Login</button>'
  389. loginForm=htmlHeader(loginCSS)
  390. loginForm+= \
  391. '<form method="POST" action="/login">' \
  392. ' <div class="imgcontainer">' \
  393. ' <img src="login.png" alt="login image" class="loginimage">'+ \
  394. loginText+TOSstr+ \
  395. ' </div>' \
  396. '' \
  397. ' <div class="container">' \
  398. ' <label for="nickname"><b>Nickname</b></label>' \
  399. ' <input type="text" placeholder="Enter Nickname" name="username" required>' \
  400. '' \
  401. ' <label for="password"><b>Password</b></label>' \
  402. ' <input type="password" placeholder="Enter Password" name="password" required>'+ \
  403. registerButtonStr+loginButtonStr+ \
  404. ' </div>' \
  405. '</form>'
  406. loginForm+=htmlFooter()
  407. return loginForm
  408. def htmlTermsOfService(baseDir: str,httpPrefix: str,domainFull: str) -> str:
  409. """Show the terms of service screen
  410. """
  411. adminNickname = getConfigParam(baseDir,'admin')
  412. if not os.path.isfile(baseDir+'/accounts/tos.txt'):
  413. copyfile(baseDir+'/default_tos.txt',baseDir+'/accounts/tos.txt')
  414. if os.path.isfile(baseDir+'/img/login-background.png'):
  415. if not os.path.isfile(baseDir+'/accounts/login-background.png'):
  416. copyfile(baseDir+'/img/login-background.png',baseDir+'/accounts/login-background.png')
  417. TOSText='Terms of Service go here.'
  418. if os.path.isfile(baseDir+'/accounts/tos.txt'):
  419. with open(baseDir+'/accounts/tos.txt', 'r') as file:
  420. TOSText = file.read()
  421. TOSForm=''
  422. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  423. termsCSS = cssFile.read()
  424. TOSForm=htmlHeader(termsCSS)
  425. TOSForm+='<div class="container">'+TOSText+'</div>'
  426. if adminNickname:
  427. adminActor=httpPrefix+'://'+domainFull+'/users/'+adminNickname
  428. TOSForm+='<div class="container"><center><p class="administeredby">Administered by <a href="'+adminActor+'">'+adminNickname+'</a></p></center></div>'
  429. TOSForm+=htmlFooter()
  430. return TOSForm
  431. def htmlHashtagBlocked(baseDir: str) -> str:
  432. """Show the screen for a blocked hashtag
  433. """
  434. blockedHashtagForm=''
  435. with open(baseDir+'/epicyon-suspended.css', 'r') as cssFile:
  436. blockedHashtagCSS=cssFile.read()
  437. blockedHashtagForm=htmlHeader(blockedHashtagCSS)
  438. blockedHashtagForm+='<div><center>'
  439. blockedHashtagForm+=' <p class="screentitle">Hashtag Blocked</p>'
  440. blockedHashtagForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
  441. blockedHashtagForm+='</center></div>'
  442. blockedHashtagForm+=htmlFooter()
  443. return blockedHashtagForm
  444. def htmlSuspended(baseDir: str) -> str:
  445. """Show the screen for suspended accounts
  446. """
  447. suspendedForm=''
  448. with open(baseDir+'/epicyon-suspended.css', 'r') as cssFile:
  449. suspendedCSS=cssFile.read()
  450. suspendedForm=htmlHeader(suspendedCSS)
  451. suspendedForm+='<div><center>'
  452. suspendedForm+=' <p class="screentitle">Account Suspended</p>'
  453. suspendedForm+=' <p>See <a href="/terms">Terms of Service</a></p>'
  454. suspendedForm+='</center></div>'
  455. suspendedForm+=htmlFooter()
  456. return suspendedForm
  457. def htmlNewPost(baseDir: str,path: str,inReplyTo: str,mentions: []) -> str:
  458. reportUrl=None
  459. if '/newreport?=' in path:
  460. reportUrl=path.split('/newreport?=')[1]
  461. path=path.split('/newreport?=')[0]
  462. replyStr=''
  463. if not path.endswith('/newshare'):
  464. if not path.endswith('/newreport'):
  465. if not inReplyTo:
  466. newPostText='<p class="new-post-text">Enter your post text below.</p>'
  467. else:
  468. newPostText='<p class="new-post-text">Enter your reply to <a href="'+inReplyTo+'">this post</a> below.</p>'
  469. replyStr='<input type="hidden" name="replyTo" value="'+inReplyTo+'">'
  470. else:
  471. newPostText= \
  472. '<p class="new-post-text">Enter your report below.</p>'
  473. # custom report header with any additional instructions
  474. if os.path.isfile(baseDir+'/accounts/report.txt'):
  475. with open(baseDir+'/accounts/report.txt', 'r') as file:
  476. customReportText=file.read()
  477. if '</p>' not in customReportText:
  478. customReportText='<p class="login-subtext">'+customReportText+'</p>'
  479. customReportText=customReportText.replace('<p>','<p class="login-subtext">')
  480. newPostText+=customReportText
  481. newPostText+='<p class="new-post-subtext">This message <i>only goes to moderators</i>, even if it mentions other fediverse addresses.</p><p class="new-post-subtext">You can also refer to points within the <a href="/terms">Terms of Service</a> if necessary.</p>'
  482. else:
  483. newPostText='<p class="new-post-text">Enter the details for your shared item below.</p>'
  484. if os.path.isfile(baseDir+'/accounts/newpost.txt'):
  485. with open(baseDir+'/accounts/newpost.txt', 'r') as file:
  486. newPostText = '<p class="new-post-text">'+file.read()+'</p>'
  487. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  488. newPostCSS = cssFile.read()
  489. pathBase=path.replace('/newreport','').replace('/newpost','').replace('/newshare','').replace('/newunlisted','').replace('/newfollowers','').replace('/newdm','')
  490. scopeIcon='scope_public.png'
  491. scopeDescription='Public'
  492. placeholderSubject='Subject or Content Warning (optional)...'
  493. placeholderMessage='Write something...'
  494. extraFields=''
  495. endpoint='newpost'
  496. if path.endswith('/newunlisted'):
  497. scopeIcon='scope_unlisted.png'
  498. scopeDescription='Unlisted'
  499. endpoint='newunlisted'
  500. if path.endswith('/newfollowers'):
  501. scopeIcon='scope_followers.png'
  502. scopeDescription='Followers Only'
  503. endpoint='newfollowers'
  504. if path.endswith('/newdm'):
  505. scopeIcon='scope_dm.png'
  506. scopeDescription='Direct Message'
  507. endpoint='newdm'
  508. if path.endswith('/newreport'):
  509. scopeIcon='scope_report.png'
  510. scopeDescription='Report'
  511. endpoint='newreport'
  512. if path.endswith('/newshare'):
  513. scopeIcon='scope_share.png'
  514. scopeDescription='Shared Item'
  515. placeholderSubject='Name of the shared item...'
  516. placeholderMessage='Description of the item being shared...'
  517. endpoint='newshare'
  518. extraFields= \
  519. '<div class="container">' \
  520. ' <input type="text" class="itemType" placeholder="Type of shared item. eg. hat" name="itemType">' \
  521. ' <input type="text" class="category" placeholder="Category of shared item. eg. clothing" name="category">' \
  522. ' <label class="labels">Duration of listing in days:</label> <input type="number" name="duration" min="1" max="365" step="1" value="14">' \
  523. '</div>' \
  524. '<input type="text" placeholder="City or location of the shared item" name="location">'
  525. newPostForm=htmlHeader(newPostCSS)
  526. # only show the share option if this is not a reply
  527. shareOptionOnDropdown=''
  528. if not replyStr:
  529. shareOptionOnDropdown='<a href="'+pathBase+'/newshare"><img src="/icons/scope_share.png"/><b>Share</b><br>Describe a shared item</a>'
  530. mentionsStr=''
  531. for m in mentions:
  532. mentionNickname=getNicknameFromActor(m)
  533. if not mentionNickname:
  534. continue
  535. mentionDomain,mentionPort=getDomainFromActor(m)
  536. if not mentionDomain:
  537. continue
  538. if mentionPort:
  539. mentionsStr+='@'+mentionNickname+'@'+mentionDomain+':'+str(mentionPort)+' '
  540. else:
  541. mentionsStr+='@'+mentionNickname+'@'+mentionDomain+' '
  542. reportOptionOnDropdown='<a href="'+pathBase+'/newreport"><img src="/icons/scope_report.png"/><b>Report</b><br>Send to moderators</a>'
  543. # For moderation reports add a link to the post reported
  544. if reportUrl:
  545. mentionStr='Reported link: '+reportUrl+'\n\n'
  546. reportOptionOnDropdown='<a href="'+pathBase+'/newreport?url='+reportUrl+'"><img src="/icons/scope_report.png"/><b>Report</b><br>Send to moderators</a>'
  547. newPostForm+= \
  548. '<form enctype="multipart/form-data" method="POST" action="'+path+'?'+endpoint+'">' \
  549. ' <div class="vertical-center">' \
  550. ' <label for="nickname"><b>'+newPostText+'</b></label>' \
  551. ' <div class="container">' \
  552. ' <div class="dropdown">' \
  553. ' <img src="/icons/'+scopeIcon+'"/><b class="scope-desc">'+scopeDescription+'</b>' \
  554. ' <div class="dropdown-content">' \
  555. ' <a href="'+pathBase+'/newpost"><img src="/icons/scope_public.png"/><b>Public</b><br>Visible to anyone</a>' \
  556. ' <a href="'+pathBase+'/newunlisted"><img src="/icons/scope_unlisted.png"/><b>Unlisted</b><br>Not on public timeline</a>' \
  557. ' <a href="'+pathBase+'/newfollowers"><img src="/icons/scope_followers.png"/><b>Followers Only</b><br>Only to followers</a>' \
  558. ' <a href="'+pathBase+'/newdm"><img src="/icons/scope_dm.png"/><b>Direct Message</b><br>Only to mentioned people</a>'+ \
  559. reportOptionOnDropdown+shareOptionOnDropdown+ \
  560. ' </div>' \
  561. ' </div>' \
  562. ' <input type="submit" name="submitPost" value="Submit">' \
  563. ' <a href="'+pathBase+'/outbox"><button class="cancelbtn">Cancel</button></a>' \
  564. ' </div>'+ \
  565. replyStr+ \
  566. ' <input type="text" placeholder="'+placeholderSubject+'" name="subject">' \
  567. '' \
  568. ' <textarea id="message" name="message" placeholder="'+placeholderMessage+'" style="height:200px" autofocus>'+mentionsStr+'</textarea>' \
  569. ''+extraFields+ \
  570. ' <div class="container">' \
  571. ' <input type="text" placeholder="Image description" name="imageDescription">' \
  572. ' <input type="file" id="attachpic" name="attachpic"' \
  573. ' accept=".png, .jpg, .jpeg, .gif">' \
  574. ' </div>' \
  575. ' </div>' \
  576. '</form>'
  577. newPostForm+=htmlFooter()
  578. return newPostForm
  579. def htmlHeader(css=None,lang='en') -> str:
  580. if not css:
  581. htmlStr= \
  582. '<!DOCTYPE html>\n' \
  583. '<html lang="'+lang+'">\n' \
  584. ' <meta charset="utf-8">\n' \
  585. ' <style>\n' \
  586. ' @import url("epicyon-profile.css");\n'+ \
  587. ' background-color: #282c37' \
  588. ' </style>\n' \
  589. ' <body>\n'
  590. else:
  591. htmlStr= \
  592. '<!DOCTYPE html>\n' \
  593. '<html lang="'+lang+'">\n' \
  594. ' <meta charset="utf-8">\n' \
  595. ' <style>\n'+css+'</style>\n' \
  596. ' <body>\n'
  597. return htmlStr
  598. def htmlFooter() -> str:
  599. htmlStr= \
  600. ' </body>\n' \
  601. '</html>\n'
  602. return htmlStr
  603. def htmlProfilePosts(baseDir: str,httpPrefix: str, \
  604. authorized: bool,ocapAlways: bool, \
  605. nickname: str,domain: str,port: int, \
  606. session,wfRequest: {},personCache: {}, \
  607. projectVersion: str) -> str:
  608. """Shows posts on the profile screen
  609. """
  610. profileStr=''
  611. outboxFeed= \
  612. personBoxJson(baseDir,domain, \
  613. port,'/users/'+nickname+'/outbox?page=1', \
  614. httpPrefix, \
  615. 4, 'outbox', \
  616. authorized, \
  617. ocapAlways)
  618. profileStr+='<script>'+contentWarningScript()+'</script>'
  619. for item in outboxFeed['orderedItems']:
  620. if item['type']=='Create' or item['type']=='Announce':
  621. profileStr+= \
  622. individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  623. nickname,domain,port,item,None,True,False, \
  624. httpPrefix,projectVersion, \
  625. False)
  626. return profileStr
  627. def htmlProfileFollowing(baseDir: str,httpPrefix: str, \
  628. authorized: bool,ocapAlways: bool, \
  629. nickname: str,domain: str,port: int, \
  630. session,wfRequest: {},personCache: {}, \
  631. followingJson: {},projectVersion: str, \
  632. buttons: []) -> str:
  633. """Shows following on the profile screen
  634. """
  635. profileStr=''
  636. for item in followingJson['orderedItems']:
  637. profileStr+= \
  638. individualFollowAsHtml(session,wfRequest,personCache, \
  639. domain,item,authorized,nickname, \
  640. httpPrefix,projectVersion, \
  641. buttons)
  642. return profileStr
  643. def htmlProfileRoles(nickname: str,domain: str,rolesJson: {}) -> str:
  644. """Shows roles on the profile screen
  645. """
  646. profileStr=''
  647. for project,rolesList in rolesJson.items():
  648. profileStr+='<div class="roles"><h2>'+project+'</h2><div class="roles-inner">'
  649. for role in rolesList:
  650. profileStr+='<h3>'+role+'</h3>'
  651. profileStr+='</div></div>'
  652. if len(profileStr)==0:
  653. profileStr+='<p>@'+nickname+'@'+domain+' has no roles assigned</p>'
  654. else:
  655. profileStr='<div>'+profileStr+'</div>'
  656. return profileStr
  657. def htmlProfileSkills(nickname: str,domain: str,skillsJson: {}) -> str:
  658. """Shows skills on the profile screen
  659. """
  660. profileStr=''
  661. for skill,level in skillsJson.items():
  662. profileStr+='<div>'+skill+'<br><div id="myProgress"><div id="myBar" style="width:'+str(level)+'%"></div></div></div><br>'
  663. if len(profileStr)==0:
  664. profileStr+='<p>@'+nickname+'@'+domain+' has no skills assigned</p>'
  665. else:
  666. profileStr='<center><div class="skill-title">'+profileStr+'</div></center>'
  667. return profileStr
  668. def htmlProfileShares(nickname: str,domain: str,sharesJson: {}) -> str:
  669. """Shows shares on the profile screen
  670. """
  671. profileStr=''
  672. for item in sharesJson['orderedItems']:
  673. profileStr+='<div class="container">'
  674. profileStr+='<p class="share-title">'+item['displayName']+'</p>'
  675. profileStr+='<a href="'+item['imageUrl']+'">'
  676. profileStr+='<img src="'+item['imageUrl']+'" alt="Item image"></a>'
  677. profileStr+='<p>'+item['summary']+'</p>'
  678. profileStr+='<p><b>Type:</b> '+item['itemType']+' '
  679. profileStr+='<b>Category:</b> '+item['category']+' '
  680. profileStr+='<b>Location:</b> '+item['location']+'</p>'
  681. profileStr+='</div>'
  682. if len(profileStr)==0:
  683. profileStr+='<p>@'+nickname+'@'+domain+' is not sharing any items</p>'
  684. else:
  685. profileStr='<div class="share-title">'+profileStr+'</div>'
  686. return profileStr
  687. def htmlProfile(projectVersion: str, \
  688. baseDir: str,httpPrefix: str,authorized: bool, \
  689. ocapAlways: bool,profileJson: {},selected: str, \
  690. session,wfRequest: {},personCache: {}, \
  691. extraJson=None) -> str:
  692. """Show the profile page as html
  693. """
  694. nickname=profileJson['name']
  695. if not nickname:
  696. return ""
  697. preferredName=profileJson['preferredUsername']
  698. domain,port=getDomainFromActor(profileJson['id'])
  699. if not domain:
  700. return ""
  701. domainFull=domain
  702. if port:
  703. domainFull=domain+':'+str(port)
  704. profileDescription=profileJson['summary']
  705. postsButton='button'
  706. followingButton='button'
  707. followersButton='button'
  708. rolesButton='button'
  709. skillsButton='button'
  710. sharesButton='button'
  711. if selected=='posts':
  712. postsButton='buttonselected'
  713. elif selected=='following':
  714. followingButton='buttonselected'
  715. elif selected=='followers':
  716. followersButton='buttonselected'
  717. elif selected=='roles':
  718. rolesButton='buttonselected'
  719. elif selected=='skills':
  720. skillsButton='buttonselected'
  721. elif selected=='shares':
  722. sharesButton='buttonselected'
  723. loginButton=''
  724. followApprovalsSection=''
  725. followApprovals=False
  726. linkToTimelineStart=''
  727. linkToTimelineEnd=''
  728. editProfileStr=''
  729. actor=profileJson['id']
  730. if not authorized:
  731. loginButton='<br><a href="/login"><button class="loginButton">Login</button></a>'
  732. else:
  733. editProfileStr='<a href="'+actor+'/editprofile"><button class="button"><span>Edit </span></button></a>'
  734. linkToTimelineStart='<a href="/users/'+nickname+'/inbox" title="Switch to timeline view" alt="Switch to timeline view">'
  735. linkToTimelineEnd='</a>'
  736. # are there any follow requests?
  737. followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
  738. if os.path.isfile(followRequestsFilename):
  739. with open(followRequestsFilename,'r') as f:
  740. for line in f:
  741. if len(line)>0:
  742. followApprovals=True
  743. followersButton='buttonhighlighted'
  744. if selected=='followers':
  745. followersButton='buttonselectedhighlighted'
  746. break
  747. if selected=='followers':
  748. if followApprovals:
  749. with open(followRequestsFilename,'r') as f:
  750. for followerHandle in f:
  751. if len(line)>0:
  752. if '://' in followerHandle:
  753. followerActor=followerHandle
  754. else:
  755. followerActor=httpPrefix+'://'+followerHandle.split('@')[1]+'/users/'+followerHandle.split('@')[0]
  756. basePath=httpPrefix+'://'+domainFull+'/users/'+nickname
  757. followApprovalsSection+='<div class="container">'
  758. followApprovalsSection+='<a href="'+followerActor+'">'
  759. followApprovalsSection+='<span class="followRequestHandle">'+followerHandle+'</span></a>'
  760. followApprovalsSection+='<a href="'+basePath+'/followapprove='+followerHandle+'">'
  761. followApprovalsSection+='<button class="followApprove">Approve</button></a>'
  762. followApprovalsSection+='<a href="'+basePath+'/followdeny='+followerHandle+'">'
  763. followApprovalsSection+='<button class="followDeny">Deny</button></a>'
  764. followApprovalsSection+='</div>'
  765. profileStr= \
  766. linkToTimelineStart+ \
  767. ' <div class="hero-image">' \
  768. ' <div class="hero-text">'+ \
  769. ' <img src="'+profileJson['icon']['url']+'" alt="'+nickname+'@'+domainFull+'">' \
  770. ' <h1>'+preferredName+'</h1>' \
  771. ' <p><b>@'+nickname+'@'+domainFull+'</b></p>' \
  772. ' <p>'+profileDescription+'</p>'+ \
  773. loginButton+ \
  774. ' </div>' \
  775. '</div>'+ \
  776. linkToTimelineEnd+ \
  777. '<div class="container">\n' \
  778. ' <center>' \
  779. ' <a href="'+actor+'"><button class="'+postsButton+'"><span>Posts </span></button></a>' \
  780. ' <a href="'+actor+'/following"><button class="'+followingButton+'"><span>Following </span></button></a>' \
  781. ' <a href="'+actor+'/followers"><button class="'+followersButton+'"><span>Followers </span></button></a>' \
  782. ' <a href="'+actor+'/roles"><button class="'+rolesButton+'"><span>Roles </span></button></a>' \
  783. ' <a href="'+actor+'/skills"><button class="'+skillsButton+'"><span>Skills </span></button></a>' \
  784. ' <a href="'+actor+'/shares"><button class="'+sharesButton+'"><span>Shares </span></button></a>'+ \
  785. editProfileStr+ \
  786. ' </center>' \
  787. '</div>'
  788. profileStr+=followApprovalsSection
  789. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  790. profileStyle = cssFile.read().replace('image.png',actor+'/image.png')
  791. if selected=='posts':
  792. profileStr+= \
  793. htmlProfilePosts(baseDir,httpPrefix,authorized, \
  794. ocapAlways,nickname,domain,port, \
  795. session,wfRequest,personCache, \
  796. projectVersion)
  797. if selected=='following':
  798. profileStr+= \
  799. htmlProfileFollowing(baseDir,httpPrefix, \
  800. authorized,ocapAlways,nickname, \
  801. domain,port,session, \
  802. wfRequest,personCache,extraJson, \
  803. projectVersion, \
  804. ["unfollow"])
  805. if selected=='followers':
  806. profileStr+= \
  807. htmlProfileFollowing(baseDir,httpPrefix, \
  808. authorized,ocapAlways,nickname, \
  809. domain,port,session, \
  810. wfRequest,personCache,extraJson, \
  811. projectVersion,
  812. ["block"])
  813. if selected=='roles':
  814. profileStr+= \
  815. htmlProfileRoles(nickname,domainFull,extraJson)
  816. if selected=='skills':
  817. profileStr+= \
  818. htmlProfileSkills(nickname,domainFull,extraJson)
  819. if selected=='shares':
  820. profileStr+= \
  821. htmlProfileShares(nickname,domainFull,extraJson)
  822. profileStr=htmlHeader(profileStyle)+profileStr+htmlFooter()
  823. return profileStr
  824. def individualFollowAsHtml(session,wfRequest: {}, \
  825. personCache: {},domain: str, \
  826. followUrl: str, \
  827. authorized: bool, \
  828. actorNickname: str, \
  829. httpPrefix: str, \
  830. projectVersion: str, \
  831. buttons=[]) -> str:
  832. nickname=getNicknameFromActor(followUrl)
  833. domain,port=getDomainFromActor(followUrl)
  834. titleStr='@'+nickname+'@'+domain
  835. avatarUrl=getPersonAvatarUrl(followUrl,personCache)
  836. if not avatarUrl:
  837. avatarUrl=followUrl+'/avatar.png'
  838. if domain not in followUrl:
  839. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,preferredName = \
  840. getPersonBox(session,wfRequest,personCache, \
  841. projectVersion,httpPrefix,domain,'outbox')
  842. if avatarUrl2:
  843. avatarUrl=avatarUrl2
  844. if preferredName:
  845. titleStr=preferredName+' '+titleStr
  846. buttonsStr=''
  847. if authorized:
  848. for b in buttons:
  849. if b=='block':
  850. buttonsStr+='<a href="/users/'+actorNickname+'?block='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">Block</button></a>'
  851. if b=='unfollow':
  852. buttonsStr+='<a href="/users/'+actorNickname+'?unfollow='+followUrl+';'+avatarUrl+'"><button class="buttonunfollow">Unfollow</button></a>'
  853. return \
  854. '<div class="container">\n' \
  855. '<a href="'+followUrl+'">' \
  856. '<p><img src="'+avatarUrl+'" alt="Avatar">\n'+ \
  857. titleStr+'</a>'+buttonsStr+'</p>' \
  858. '</div>\n'
  859. def contentWarningScript() -> str:
  860. """Returns a script used for content warnings
  861. """
  862. script= \
  863. 'function showContentWarning(postID) {' \
  864. ' var x = document.getElementById(postID);' \
  865. ' if (x.style.display === "none") {' \
  866. ' x.style.display = "block";' \
  867. ' } else {' \
  868. ' x.style.display = "none";' \
  869. ' }' \
  870. '}'
  871. return script
  872. def individualPostAsHtml(baseDir: str, \
  873. session,wfRequest: {},personCache: {}, \
  874. nickname: str,domain: str,port: int, \
  875. postJsonObject: {}, \
  876. avatarUrl: str, showAvatarDropdown: bool,
  877. allowDeletion: bool, \
  878. httpPrefix: str, projectVersion: str, \
  879. showIcons=False) -> str:
  880. """ Shows a single post as html
  881. """
  882. titleStr=''
  883. if postJsonObject['type']=='Announce':
  884. if postJsonObject.get('object'):
  885. if isinstance(postJsonObject['object'], str):
  886. # get the announced post
  887. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  888. announcedJson = getJson(session,postJsonObject['object'],asHeader,None,projectVersion,httpPrefix,domain)
  889. if announcedJson:
  890. if not announcedJson.get('type'):
  891. return ''
  892. if announcedJson['type']!='Create':
  893. return ''
  894. actorNickname=getNicknameFromActor(postJsonObject['actor'])
  895. actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
  896. titleStr+='@'+actorNickname+'@'+actorDomain+' announced:<br>'
  897. postJsonObject=announcedJson
  898. else:
  899. return ''
  900. else:
  901. return ''
  902. if not isinstance(postJsonObject['object'], dict):
  903. return ''
  904. isModerationPost=False
  905. if postJsonObject['object'].get('moderationStatus'):
  906. isModerationPost=True
  907. avatarPosition=''
  908. containerClass='container'
  909. containerClassIcons='containericons'
  910. timeClass='time-right'
  911. actorNickname=getNicknameFromActor(postJsonObject['actor'])
  912. actorDomain,actorPort=getDomainFromActor(postJsonObject['actor'])
  913. messageId=''
  914. if postJsonObject.get('id'):
  915. messageId=postJsonObject['id'].replace('/activity','')
  916. titleStr+='<a href="'+messageId+'">@'+actorNickname+'@'+actorDomain+'</a>'
  917. if postJsonObject['object']['inReplyTo']:
  918. containerClassIcons='containericons darker'
  919. containerClass='container darker'
  920. avatarPosition=' class="right"'
  921. timeClass='time-left'
  922. if '/statuses/' in postJsonObject['object']['inReplyTo']:
  923. replyNickname=getNicknameFromActor(postJsonObject['object']['inReplyTo'])
  924. replyDomain,replyPort=getDomainFromActor(postJsonObject['object']['inReplyTo'])
  925. if replyNickname and replyDomain:
  926. titleStr+=' <i class="replyingto">replying to</i> <a href="'+postJsonObject['object']['inReplyTo']+'">@'+replyNickname+'@'+replyDomain+'</a>'
  927. else:
  928. postDomain=postJsonObject['object']['inReplyTo'].replace('https://','').replace('http://','').replace('dat://','')
  929. if '/' in postDomain:
  930. postDomain=postDomain.split('/',1)[0]
  931. titleStr+=' <i class="replyingto">replying to</i> <a href="'+postJsonObject['object']['inReplyTo']+'">'+postDomain+'</a>'
  932. attachmentStr=''
  933. if postJsonObject['object']['attachment']:
  934. if isinstance(postJsonObject['object']['attachment'], list):
  935. attachmentCtr=0
  936. for attach in postJsonObject['object']['attachment']:
  937. if attach.get('mediaType') and attach.get('url'):
  938. mediaType=attach['mediaType']
  939. imageDescription=''
  940. if attach.get('name'):
  941. imageDescription=attach['name']
  942. if mediaType=='image/png' or \
  943. mediaType=='image/jpeg' or \
  944. mediaType=='image/gif':
  945. if attach['url'].endswith('.png') or \
  946. attach['url'].endswith('.jpg') or \
  947. attach['url'].endswith('.jpeg') or \
  948. attach['url'].endswith('.gif'):
  949. if attachmentCtr>0:
  950. attachmentStr+='<br>'
  951. attachmentStr+= \
  952. '<a href="'+attach['url']+'">' \
  953. '<img src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment"></a>\n'
  954. attachmentCtr+=1
  955. if not avatarUrl:
  956. avatarUrl=getPersonAvatarUrl(postJsonObject['actor'],personCache)
  957. if not avatarUrl:
  958. avatarUrl=postJsonObject['actor']+'/avatar.png'
  959. fullDomain=domain
  960. if port:
  961. if port!=80 and port!=443:
  962. if ':' not in domain:
  963. fullDomain=domain+':'+str(port)
  964. if fullDomain not in postJsonObject['actor']:
  965. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl2,preferredName = \
  966. getPersonBox(session,wfRequest,personCache, \
  967. projectVersion,httpPrefix,domain,'outbox')
  968. if avatarUrl2:
  969. avatarUrl=avatarUrl2
  970. if preferredName:
  971. titleStr=preferredName+' '+titleStr
  972. avatarDropdown= \
  973. ' <a href="'+postJsonObject['actor']+'">' \
  974. ' <img src="'+avatarUrl+'" title="Show profile" alt="Avatar"'+avatarPosition+'/></a>'
  975. if showAvatarDropdown and fullDomain+'/users/'+nickname not in postJsonObject['actor']:
  976. # if not following then show "Follow" in the dropdown
  977. followUnfollowStr='<a href="/users/'+nickname+'?follow='+postJsonObject['actor']+';'+avatarUrl+'">Follow</a>'
  978. # if following then show "Unfollow" in the dropdown
  979. if isFollowingActor(baseDir,nickname,domain,postJsonObject['actor']):
  980. followUnfollowStr='<a href="/users/'+nickname+'?unfollow='+postJsonObject['actor']+';'+avatarUrl+'">Unfollow</a>'
  981. blockUnblockStr='<a href="/users/'+nickname+'?block='+postJsonObject['actor']+';'+avatarUrl+'">Block</a>'
  982. # if blocking then show "Unblock" in the dropdown
  983. actorDomainFull=actorDomain
  984. if actorPort:
  985. if actorPort!=80 and actorPort!=443:
  986. if ':' not in actorDomain:
  987. actorDomainFull=actorDomain+':'+str(actorPort)
  988. if isBlocked(baseDir,nickname,domain,actorNickname,actorDomainFull):
  989. blockUnblockStr='<a href="/users/'+nickname+'?unblock='+postJsonObject['actor']+';'+avatarUrl+'">Unblock</a>'
  990. reportStr=''
  991. if messageId:
  992. reportStr='<a href="/users/'+nickname+'/newreport?url='+messageId+';'+avatarUrl+'">Report</a>'
  993. avatarDropdown= \
  994. ' <div class="dropdown-timeline">' \
  995. ' <img src="'+avatarUrl+'" '+avatarPosition+'/>' \
  996. ' <div class="dropdown-timeline-content">' \
  997. ' <a href="'+postJsonObject['actor']+'">Visit</a>'+ \
  998. followUnfollowStr+blockUnblockStr+reportStr+ \
  999. ' </div>' \
  1000. ' </div>'
  1001. publishedStr=postJsonObject['object']['published']
  1002. datetimeObject = datetime.strptime(publishedStr,"%Y-%m-%dT%H:%M:%SZ")
  1003. publishedStr=datetimeObject.strftime("%a %b %d, %H:%M")
  1004. footerStr='<span class="'+timeClass+'">'+publishedStr+'</span>\n'
  1005. announceStr=''
  1006. if not isModerationPost:
  1007. # don't allow announce/repeat of your own posts
  1008. announceIcon='repeat_inactive.png'
  1009. announceLink='repeat'
  1010. announceTitle='Repeat this post'
  1011. if announcedByPerson(postJsonObject,nickname,fullDomain):
  1012. announceIcon='repeat.png'
  1013. announceLink='unrepeat'
  1014. announceTitle='Undo the repeat this post'
  1015. announceStr= \
  1016. '<a href="/users/'+nickname+'?'+announceLink+'='+postJsonObject['object']['id']+'" title="'+announceTitle+'">' \
  1017. '<img src="/icons/'+announceIcon+'"/></a>'
  1018. likeStr=''
  1019. if not isModerationPost:
  1020. likeIcon='like_inactive.png'
  1021. likeLink='like'
  1022. likeTitle='Like this post'
  1023. if likedByPerson(postJsonObject,nickname,fullDomain):
  1024. likeIcon='like.png'
  1025. likeLink='unlike'
  1026. likeTitle='Undo the like of this post'
  1027. likeStr= \
  1028. '<a href="/users/'+nickname+'?'+likeLink+'='+postJsonObject['object']['id']+'" title="'+likeTitle+'">' \
  1029. '<img src="/icons/'+likeIcon+'"/></a>'
  1030. deleteStr=''
  1031. if allowDeletion or \
  1032. ('/'+fullDomain+'/' in postJsonObject['actor'] and \
  1033. postJsonObject['object']['id'].startswith(postJsonObject['actor'])):
  1034. if '/users/'+nickname+'/' in postJsonObject['object']['id']:
  1035. deleteStr= \
  1036. '<a href="/users/'+nickname+'?delete='+postJsonObject['object']['id']+'" title="Delete this post">' \
  1037. '<img src="/icons/delete.png"/></a>'
  1038. if showIcons:
  1039. replyToLink=postJsonObject['object']['id']
  1040. if postJsonObject['object'].get('attributedTo'):
  1041. replyToLink+='?mention='+postJsonObject['object']['attributedTo']
  1042. if postJsonObject['object'].get('content'):
  1043. mentionedActors=getMentionsFromHtml(postJsonObject['object']['content'])
  1044. if mentionedActors:
  1045. for actorUrl in mentionedActors:
  1046. if '?mention='+actorUrl not in replyToLink:
  1047. replyToLink+='?mention='+actorUrl
  1048. if len(replyToLink)>500:
  1049. break
  1050. footerStr='<div class="'+containerClassIcons+'">'
  1051. if not isModerationPost:
  1052. footerStr+='<a href="/users/'+nickname+'?replyto='+replyToLink+'" title="Reply to this post">'
  1053. else:
  1054. footerStr+='<a href="/users/'+nickname+'?replydm='+replyToLink+'" title="Reply to this post">'
  1055. footerStr+='<img src="/icons/reply.png"/></a>'
  1056. footerStr+=announceStr+likeStr+deleteStr
  1057. footerStr+='<span class="'+timeClass+'">'+publishedStr+'</span>'
  1058. footerStr+='</div>'
  1059. if not postJsonObject['object']['sensitive']:
  1060. contentStr=postJsonObject['object']['content']+attachmentStr
  1061. else:
  1062. postID='post'+str(createPassword(8))
  1063. contentStr=''
  1064. if postJsonObject['object'].get('summary'):
  1065. contentStr+='<b>'+postJsonObject['object']['summary']+'</b> '
  1066. if isModerationPost:
  1067. containerClass='container report'
  1068. else:
  1069. contentStr+='<b>Sensitive</b> '
  1070. contentStr+='<button class="cwButton" onclick="showContentWarning('+"'"+postID+"'"+')">SHOW MORE</button>'
  1071. contentStr+='<div class="cwText" id="'+postID+'">'
  1072. contentStr+=postJsonObject['object']['content']+attachmentStr
  1073. contentStr+='</div>'
  1074. return \
  1075. '<div class="'+containerClass+'">\n'+ \
  1076. avatarDropdown+ \
  1077. '<p class="post-title">'+titleStr+'</p>'+ \
  1078. contentStr+footerStr+ \
  1079. '</div>\n'
  1080. def htmlTimeline(pageNumber: int,itemsPerPage: int,session,baseDir: str, \
  1081. wfRequest: {},personCache: {}, \
  1082. nickname: str,domain: str,port: int,timelineJson: {}, \
  1083. boxName: str,allowDeletion: bool, \
  1084. httpPrefix: str,projectVersion: str) -> str:
  1085. """Show the timeline as html
  1086. """
  1087. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  1088. profileStyle = \
  1089. cssFile.read().replace('banner.png', \
  1090. '/users/'+nickname+'/banner.png')
  1091. moderator=isModerator(baseDir,nickname)
  1092. inboxButton='button'
  1093. sentButton='button'
  1094. moderationButton='button'
  1095. if boxName=='inbox':
  1096. inboxButton='buttonselected'
  1097. elif boxName=='outbox':
  1098. sentButton='buttonselected'
  1099. elif boxName=='moderation':
  1100. moderationButton='buttonselected'
  1101. actor='/users/'+nickname
  1102. showIndividualPostIcons=True
  1103. if boxName=='inbox':
  1104. showIndividualPostIcons=True
  1105. followApprovals=''
  1106. followRequestsFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/followrequests.txt'
  1107. if os.path.isfile(followRequestsFilename):
  1108. with open(followRequestsFilename,'r') as f:
  1109. for line in f:
  1110. if len(line)>0:
  1111. # show follow approvals icon
  1112. followApprovals='<a href="'+actor+'/followers"><img class="right" alt="Approve follow requests" title="Approve follow requests" src="/icons/person.png"/></a>'
  1113. break
  1114. moderationButtonStr=''
  1115. if moderator:
  1116. moderationButtonStr='<a href="'+actor+'/moderation"><button class="'+moderationButton+'"><span>Moderate </span></button></a>'
  1117. tlStr=htmlHeader(profileStyle)
  1118. # banner and row of buttons
  1119. tlStr+= \
  1120. '<a href="/users/'+nickname+'" title="Switch to profile view" alt="Switch to profile view">' \
  1121. '<div class="timeline-banner">' \
  1122. '</div></a>' \
  1123. '<div class="container">\n'+ \
  1124. ' <a href="'+actor+'/inbox"><button class="'+inboxButton+'"><span>Inbox </span></button></a>' \
  1125. ' <a href="'+actor+'/outbox"><button class="'+sentButton+'"><span>Sent </span></button></a>'+ \
  1126. moderationButtonStr+ \
  1127. ' <a href="'+actor+'/newpost"><img src="/icons/newpost.png" title="Create a new post" alt="Create a new post" class="right"/></a>'+ \
  1128. ' <a href="'+actor+'/search"><img src="/icons/search.png" title="Search and follow" alt="Search and follow" class="right"/></a>'+ \
  1129. followApprovals+ \
  1130. '</div>'
  1131. # second row of buttons for moderator actions
  1132. if moderator and boxName=='moderation':
  1133. tlStr+= \
  1134. '<form method="POST" action="/users/'+nickname+'/moderationaction">' \
  1135. '<div class="container">\n'+ \
  1136. ' <input type="text" placeholder="Nickname or URL. Block using *@domain or nickname@domain" name="moderationAction" value="">' \
  1137. ' <input type="submit" title="Remove the above item" name="submitRemove" value="Remove">' \
  1138. ' <input type="submit" title="Suspend the above account nickname" name="submitSuspend" value="Suspend">' \
  1139. ' <input type="submit" title="Remove a suspension for an account nickname" name="submitUnsuspend" value="Unsuspend">' \
  1140. ' <input type="submit" title="Block an account on another instance" name="submitBlock" value="Block">' \
  1141. ' <input type="submit" title="Unblock an account on another instance" name="submitUnblock" value="Unblock">' \
  1142. ' <input type="submit" title="Information about current blocks/suspensions" name="submitInfo" value="Info">' \
  1143. '</div></form>'
  1144. # add the javascript for content warnings
  1145. tlStr+='<script>'+contentWarningScript()+'</script>'
  1146. # page up arrow
  1147. if pageNumber>1:
  1148. tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber-1)+'"><img class="pageicon" src="/icons/pageup.png" title="Page up" alt="Page up"></a></center>'
  1149. # show the posts
  1150. itemCtr=0
  1151. for item in timelineJson['orderedItems']:
  1152. if item['type']=='Create' or item['type']=='Announce':
  1153. itemCtr+=1
  1154. avatarUrl=getPersonAvatarUrl(item['actor'],personCache)
  1155. tlStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  1156. nickname,domain,port,item,avatarUrl,True, \
  1157. allowDeletion, \
  1158. httpPrefix,projectVersion,
  1159. showIndividualPostIcons)
  1160. # page down arrow
  1161. if itemCtr>=itemsPerPage:
  1162. tlStr+='<center><a href="'+actor+'/'+boxName+'?page='+str(pageNumber+1)+'"><img class="pageicon" src="/icons/pagedown.png" title="Page down" alt="Page down"></a></center>'
  1163. tlStr+=htmlFooter()
  1164. return tlStr
  1165. def htmlInbox(pageNumber: int,itemsPerPage: int, \
  1166. session,baseDir: str,wfRequest: {},personCache: {}, \
  1167. nickname: str,domain: str,port: int,inboxJson: {}, \
  1168. allowDeletion: bool, \
  1169. httpPrefix: str,projectVersion: str) -> str:
  1170. """Show the inbox as html
  1171. """
  1172. return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
  1173. nickname,domain,port,inboxJson,'inbox',allowDeletion, \
  1174. httpPrefix,projectVersion)
  1175. def htmlModeration(pageNumber: int,itemsPerPage: int, \
  1176. session,baseDir: str,wfRequest: {},personCache: {}, \
  1177. nickname: str,domain: str,port: int,inboxJson: {}, \
  1178. allowDeletion: bool, \
  1179. httpPrefix: str,projectVersion: str) -> str:
  1180. """Show the moderation feed as html
  1181. """
  1182. return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
  1183. nickname,domain,port,inboxJson,'moderation',allowDeletion, \
  1184. httpPrefix,projectVersion)
  1185. def htmlOutbox(pageNumber: int,itemsPerPage: int, \
  1186. session,baseDir: str,wfRequest: {},personCache: {}, \
  1187. nickname: str,domain: str,port: int,outboxJson: {}, \
  1188. allowDeletion: bool,
  1189. httpPrefix: str,projectVersion: str) -> str:
  1190. """Show the Outbox as html
  1191. """
  1192. return htmlTimeline(pageNumber,itemsPerPage,session,baseDir,wfRequest,personCache, \
  1193. nickname,domain,port,outboxJson,'outbox',allowDeletion, \
  1194. httpPrefix,projectVersion)
  1195. def htmlIndividualPost(baseDir: str,session,wfRequest: {},personCache: {}, \
  1196. nickname: str,domain: str,port: int,authorized: bool, \
  1197. postJsonObject: {},httpPrefix: str,projectVersion: str) -> str:
  1198. """Show an individual post as html
  1199. """
  1200. postStr='<script>'+contentWarningScript()+'</script>'
  1201. postStr+= \
  1202. individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  1203. nickname,domain,port,postJsonObject,None,True,False, \
  1204. httpPrefix,projectVersion,False)
  1205. messageId=postJsonObject['id'].replace('/activity','')
  1206. # show the previous posts
  1207. while postJsonObject['object'].get('inReplyTo'):
  1208. postFilename=locatePost(baseDir,nickname,domain,postJsonObject['object']['inReplyTo'])
  1209. if not postFilename:
  1210. break
  1211. with open(postFilename, 'r') as fp:
  1212. postJsonObject=commentjson.load(fp)
  1213. postStr= \
  1214. individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  1215. nickname,domain,port,postJsonObject, \
  1216. None,True,False, \
  1217. httpPrefix,projectVersion, \
  1218. False)+postStr
  1219. # show the following posts
  1220. postFilename=locatePost(baseDir,nickname,domain,messageId)
  1221. if postFilename:
  1222. # is there a replies file for this post?
  1223. repliesFilename=postFilename.replace('.json','.replies')
  1224. if os.path.isfile(repliesFilename):
  1225. # get items from the replies file
  1226. repliesJson={'orderedItems': []}
  1227. populateRepliesJson(baseDir,nickname,domain,repliesFilename,authorized,repliesJson)
  1228. # add items to the html output
  1229. for item in repliesJson['orderedItems']:
  1230. postStr+= \
  1231. individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  1232. nickname,domain,port,item,None,True,False, \
  1233. httpPrefix,projectVersion,False)
  1234. return htmlHeader()+postStr+htmlFooter()
  1235. def htmlPostReplies(baseDir: str,session,wfRequest: {},personCache: {}, \
  1236. nickname: str,domain: str,port: int,repliesJson: {}, \
  1237. httpPrefix: str,projectVersion: str) -> str:
  1238. """Show the replies to an individual post as html
  1239. """
  1240. repliesStr=''
  1241. if repliesJson.get('orderedItems'):
  1242. for item in repliesJson['orderedItems']:
  1243. repliesStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \
  1244. nickname,domain,port,item,None,True,False, \
  1245. httpPrefix,projectVersion,False)
  1246. return htmlHeader()+repliesStr+htmlFooter()
  1247. def htmlFollowConfirm(baseDir: str,originPathStr: str,followActor: str,followProfileUrl: str) -> str:
  1248. """Asks to confirm a follow
  1249. """
  1250. followDomain,port=getDomainFromActor(followActor)
  1251. if os.path.isfile(baseDir+'/img/follow-background.png'):
  1252. if not os.path.isfile(baseDir+'/accounts/follow-background.png'):
  1253. copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png')
  1254. with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  1255. profileStyle = cssFile.read()
  1256. followStr=htmlHeader(profileStyle)
  1257. followStr+='<div class="follow">'
  1258. followStr+=' <div class="followAvatar">'
  1259. followStr+=' <center>'
  1260. followStr+=' <a href="'+followActor+'">'
  1261. followStr+=' <img src="'+followProfileUrl+'"/></a>'
  1262. followStr+=' <p class="followText">Follow '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
  1263. followStr+= \
  1264. ' <form method="POST" action="'+originPathStr+'/followconfirm">' \
  1265. ' <input type="hidden" name="actor" value="'+followActor+'">' \
  1266. ' <button type="submit" class="button" name="submitYes">Yes</button>' \
  1267. ' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
  1268. ' </form>'
  1269. followStr+='</center>'
  1270. followStr+='</div>'
  1271. followStr+='</div>'
  1272. followStr+=htmlFooter()
  1273. return followStr
  1274. def htmlUnfollowConfirm(baseDir: str,originPathStr: str,followActor: str,followProfileUrl: str) -> str:
  1275. """Asks to confirm unfollowing an actor
  1276. """
  1277. followDomain,port=getDomainFromActor(followActor)
  1278. if os.path.isfile(baseDir+'/img/follow-background.png'):
  1279. if not os.path.isfile(baseDir+'/accounts/follow-background.png'):
  1280. copyfile(baseDir+'/img/follow-background.png',baseDir+'/accounts/follow-background.png')
  1281. with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  1282. profileStyle = cssFile.read()
  1283. followStr=htmlHeader(profileStyle)
  1284. followStr+='<div class="follow">'
  1285. followStr+=' <div class="followAvatar">'
  1286. followStr+=' <center>'
  1287. followStr+=' <a href="'+followActor+'">'
  1288. followStr+=' <img src="'+followProfileUrl+'"/></a>'
  1289. followStr+=' <p class="followText">Stop following '+getNicknameFromActor(followActor)+'@'+followDomain+' ?</p>'
  1290. followStr+= \
  1291. ' <form method="POST" action="'+originPathStr+'/unfollowconfirm">' \
  1292. ' <input type="hidden" name="actor" value="'+followActor+'">' \
  1293. ' <button type="submit" class="button" name="submitYes">Yes</button>' \
  1294. ' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
  1295. ' </form>'
  1296. followStr+='</center>'
  1297. followStr+='</div>'
  1298. followStr+='</div>'
  1299. followStr+=htmlFooter()
  1300. return followStr
  1301. def htmlBlockConfirm(baseDir: str,originPathStr: str,blockActor: str,blockProfileUrl: str) -> str:
  1302. """Asks to confirm a block
  1303. """
  1304. blockDomain,port=getDomainFromActor(blockActor)
  1305. if os.path.isfile(baseDir+'/img/block-background.png'):
  1306. if not os.path.isfile(baseDir+'/accounts/block-background.png'):
  1307. copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
  1308. with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  1309. profileStyle = cssFile.read()
  1310. blockStr=htmlHeader(profileStyle)
  1311. blockStr+='<div class="block">'
  1312. blockStr+=' <div class="blockAvatar">'
  1313. blockStr+=' <center>'
  1314. blockStr+=' <a href="'+blockActor+'">'
  1315. blockStr+=' <img src="'+blockProfileUrl+'"/></a>'
  1316. blockStr+=' <p class="blockText">Block '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
  1317. blockStr+= \
  1318. ' <form method="POST" action="'+originPathStr+'/blockconfirm">' \
  1319. ' <input type="hidden" name="actor" value="'+blockActor+'">' \
  1320. ' <button type="submit" class="button" name="submitYes">Yes</button>' \
  1321. ' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
  1322. ' </form>'
  1323. blockStr+='</center>'
  1324. blockStr+='</div>'
  1325. blockStr+='</div>'
  1326. blockStr+=htmlFooter()
  1327. return blockStr
  1328. def htmlUnblockConfirm(baseDir: str,originPathStr: str,blockActor: str,blockProfileUrl: str) -> str:
  1329. """Asks to confirm unblocking an actor
  1330. """
  1331. blockDomain,port=getDomainFromActor(blockActor)
  1332. if os.path.isfile(baseDir+'/img/block-background.png'):
  1333. if not os.path.isfile(baseDir+'/accounts/block-background.png'):
  1334. copyfile(baseDir+'/img/block-background.png',baseDir+'/accounts/block-background.png')
  1335. with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  1336. profileStyle = cssFile.read()
  1337. blockStr=htmlHeader(profileStyle)
  1338. blockStr+='<div class="block">'
  1339. blockStr+=' <div class="blockAvatar">'
  1340. blockStr+=' <center>'
  1341. blockStr+=' <a href="'+blockActor+'">'
  1342. blockStr+=' <img src="'+blockProfileUrl+'"/></a>'
  1343. blockStr+=' <p class="blockText">Stop blocking '+getNicknameFromActor(blockActor)+'@'+blockDomain+' ?</p>'
  1344. blockStr+= \
  1345. ' <form method="POST" action="'+originPathStr+'/unblockconfirm">' \
  1346. ' <input type="hidden" name="actor" value="'+blockActor+'">' \
  1347. ' <button type="submit" class="button" name="submitYes">Yes</button>' \
  1348. ' <a href="'+originPathStr+'"><button class="button">No</button></a>' \
  1349. ' </form>'
  1350. blockStr+='</center>'
  1351. blockStr+='</div>'
  1352. blockStr+='</div>'
  1353. blockStr+=htmlFooter()
  1354. return blockStr
  1355. def htmlSearch(baseDir: str,path: str) -> str:
  1356. """Search called from the timeline icon
  1357. """
  1358. actor=path.replace('/search','')
  1359. nickname=getNicknameFromActor(actor)
  1360. domain,port=getDomainFromActor(actor)
  1361. if os.path.isfile(baseDir+'/img/search-background.png'):
  1362. if not os.path.isfile(baseDir+'/accounts/search-background.png'):
  1363. copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
  1364. with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
  1365. profileStyle = cssFile.read()
  1366. followStr=htmlHeader(profileStyle)
  1367. followStr+='<div class="follow">'
  1368. followStr+=' <div class="followAvatar">'
  1369. followStr+=' <center>'
  1370. followStr+=' <p class="followText">Enter an address to search for</p>'
  1371. followStr+= \
  1372. ' <form method="POST" action="'+actor+'/searchhandle">' \
  1373. ' <input type="hidden" name="actor" value="'+actor+'">' \
  1374. ' <input type="text" name="searchtext" autofocus><br>' \
  1375. ' <button type="submit" class="button" name="submitSearch">Submit</button>' \
  1376. ' <a href="'+actor+'"><button class="button">Go Back</button></a>' \
  1377. ' </form>'
  1378. followStr+=' </center>'
  1379. followStr+=' </div>'
  1380. followStr+='</div>'
  1381. followStr+=htmlFooter()
  1382. return followStr
  1383. def htmlProfileAfterSearch(baseDir: str,path: str,httpPrefix: str, \
  1384. nickname: str,domain: str,port: int, \
  1385. profileHandle: str, \
  1386. session,wfRequest: {},personCache: {},
  1387. debug: bool,projectVersion: str) -> str:
  1388. """Show a profile page after a search for a fediverse address
  1389. """
  1390. if '/users/' in profileHandle:
  1391. searchNickname=getNicknameFromActor(profileHandle)
  1392. searchDomain,searchPort=getDomainFromActor(profileHandle)
  1393. else:
  1394. if '@' not in profileHandle:
  1395. if debug:
  1396. print('DEBUG: no @ in '+profileHandle)
  1397. return None
  1398. if profileHandle.startswith('@'):
  1399. profileHandle=profileHandle[1:]
  1400. if '@' not in profileHandle:
  1401. if debug:
  1402. print('DEBUG: no @ in '+profileHandle)
  1403. return None
  1404. searchNickname=profileHandle.split('@')[0]
  1405. searchDomain=profileHandle.split('@')[1]
  1406. searchPort=None
  1407. if ':' in searchDomain:
  1408. searchPort=int(searchDomain.split(':')[1])
  1409. searchDomain=searchDomain.split(':')[0]
  1410. if not searchNickname:
  1411. if debug:
  1412. print('DEBUG: No nickname found in '+profileHandle)
  1413. return None
  1414. if not searchDomain:
  1415. if debug:
  1416. print('DEBUG: No domain found in '+profileHandle)
  1417. return None
  1418. searchDomainFull=searchDomain
  1419. if searchPort:
  1420. if searchPort!=80 and searchPort!=443:
  1421. if ':' not in searchDomain:
  1422. searchDomainFull=searchDomain+':'+str(searchPort)
  1423. profileStr=''
  1424. with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
  1425. wf = webfingerHandle(session,searchNickname+'@'+searchDomainFull,httpPrefix,wfRequest, \
  1426. domain,projectVersion)
  1427. if not wf:
  1428. if debug:
  1429. print('DEBUG: Unable to webfinger '+searchNickname+'@'+searchDomainFull)
  1430. return None
  1431. asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
  1432. personUrl = getUserUrl(wf)
  1433. profileJson = getJson(session,personUrl,asHeader,None,projectVersion,httpPrefix,domain)
  1434. if not profileJson:
  1435. if debug:
  1436. print('DEBUG: No actor returned from '+personUrl)
  1437. return None
  1438. avatarUrl=''
  1439. if profileJson.get('icon'):
  1440. if profileJson['icon'].get('url'):
  1441. avatarUrl=profileJson['icon']['url']
  1442. if not avatarUrl:
  1443. avatarUrl=getPersonAvatarUrl(personUrl,personCache)
  1444. preferredName=searchNickname
  1445. if profileJson.get('preferredUsername'):
  1446. preferredName=profileJson['preferredUsername']
  1447. profileDescription=''
  1448. if profileJson.get('summary'):
  1449. profileDescription=profileJson['summary']
  1450. outboxUrl=None
  1451. if not profileJson.get('outbox'):
  1452. if debug:
  1453. pprint(profileJson)
  1454. print('DEBUG: No outbox found')
  1455. return None
  1456. outboxUrl=profileJson['outbox']
  1457. profileBackgroundImage=''
  1458. if profileJson.get('image'):
  1459. if profileJson['image'].get('url'):
  1460. profileBackgroundImage=profileJson['image']['url']
  1461. profileStyle = cssFile.read().replace('image.png',profileBackgroundImage)
  1462. # url to return to
  1463. backUrl=path
  1464. if not backUrl.endswith('/inbox'):
  1465. backUrl+='/inbox'
  1466. profileStr= \
  1467. ' <div class="hero-image">' \
  1468. ' <div class="hero-text">' \
  1469. ' <img src="'+avatarUrl+'" alt="'+searchNickname+'@'+searchDomainFull+'">' \
  1470. ' <h1>'+preferredName+'</h1>' \
  1471. ' <p><b>@'+searchNickname+'@'+searchDomainFull+'</b></p>' \
  1472. ' <p>'+profileDescription+'</p>'+ \
  1473. ' </div>' \
  1474. '</div>'+ \
  1475. '<div class="container">\n' \
  1476. ' <form method="POST" action="'+backUrl+'/followconfirm">' \
  1477. ' <center>' \
  1478. ' <input type="hidden" name="actor" value="'+personUrl+'">' \
  1479. ' <button type="submit" class="button" name="submitYes">Follow</button>' \
  1480. ' <a href="'+backUrl+'"><button class="button">Go Back</button></a>' \
  1481. ' </center>' \
  1482. ' </form>' \
  1483. '</div>'
  1484. profileStr+='<script>'+contentWarningScript()+'</script>'
  1485. result = []
  1486. i = 0
  1487. for item in parseUserFeed(session,outboxUrl,asHeader, \
  1488. projectVersion,httpPrefix,domain):
  1489. if not item.get('type'):
  1490. continue
  1491. if item['type']!='Create' and item['type']!='Announce':
  1492. continue
  1493. if not item.get('object'):
  1494. continue
  1495. profileStr+= \
  1496. individualPostAsHtml(baseDir, \
  1497. session,wfRequest,personCache, \
  1498. nickname,domain,port, \
  1499. item,avatarUrl,False,False, \
  1500. httpPrefix,projectVersion,False)
  1501. i+=1
  1502. if i>=20:
  1503. break
  1504. return htmlHeader(profileStyle)+profileStr+htmlFooter()