Modern ActivityPub compliant server, designed for simplicity and accessibility. Includes calendar and sharing economy features to empower your federated community. https://code.freedombone.net/bashrc/epicyon Docs: https://epicyon.net/#install
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

webapp_moderation.py 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  1. __filename__ = "webapp_moderation.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.2.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import os
  9. from utils import getFullDomain
  10. from utils import isEditor
  11. from utils import loadJson
  12. from utils import getNicknameFromActor
  13. from utils import getDomainFromActor
  14. from utils import getConfigParam
  15. from posts import downloadFollowCollection
  16. from posts import getPublicPostInfo
  17. from posts import isModerator
  18. from webapp_timeline import htmlTimeline
  19. # from webapp_utils import getPersonAvatarUrl
  20. from webapp_utils import getContentWarningButton
  21. from webapp_utils import htmlHeaderWithExternalStyle
  22. from webapp_utils import htmlFooter
  23. from blocking import isBlockedDomain
  24. from blocking import isBlocked
  25. from session import createSession
  26. def htmlModeration(cssCache: {}, defaultTimeline: str,
  27. recentPostsCache: {}, maxRecentPosts: int,
  28. translate: {}, pageNumber: int, itemsPerPage: int,
  29. session, baseDir: str, wfRequest: {}, personCache: {},
  30. nickname: str, domain: str, port: int, inboxJson: {},
  31. allowDeletion: bool,
  32. httpPrefix: str, projectVersion: str,
  33. YTReplacementDomain: str,
  34. showPublishedDateOnly: bool,
  35. newswire: {}, positiveVoting: bool,
  36. showPublishAsIcon: bool,
  37. fullWidthTimelineButtonHeader: bool,
  38. iconsAsButtons: bool,
  39. rssIconAtTop: bool,
  40. publishButtonAtTop: bool,
  41. authorized: bool, moderationActionStr: str,
  42. theme: str, peertubeInstances: [],
  43. allowLocalNetworkAccess: bool,
  44. textModeBanner: str,
  45. accessKeys: {}) -> str:
  46. """Show the moderation feed as html
  47. This is what you see when selecting the "mod" timeline
  48. """
  49. return htmlTimeline(cssCache, defaultTimeline,
  50. recentPostsCache, maxRecentPosts,
  51. translate, pageNumber,
  52. itemsPerPage, session, baseDir, wfRequest, personCache,
  53. nickname, domain, port, inboxJson, 'moderation',
  54. allowDeletion, httpPrefix, projectVersion, True, False,
  55. YTReplacementDomain, showPublishedDateOnly,
  56. newswire, False, False, positiveVoting,
  57. showPublishAsIcon, fullWidthTimelineButtonHeader,
  58. iconsAsButtons, rssIconAtTop, publishButtonAtTop,
  59. authorized, moderationActionStr, theme,
  60. peertubeInstances, allowLocalNetworkAccess,
  61. textModeBanner, accessKeys)
  62. def htmlAccountInfo(cssCache: {}, translate: {},
  63. baseDir: str, httpPrefix: str,
  64. nickname: str, domain: str, port: int,
  65. searchHandle: str, debug: bool) -> str:
  66. """Shows which domains a search handle interacts with.
  67. This screen is shown if a moderator enters a handle and selects info
  68. on the moderation screen
  69. """
  70. msgStr1 = 'This account interacts with the following instances'
  71. infoForm = ''
  72. cssFilename = baseDir + '/epicyon-profile.css'
  73. if os.path.isfile(baseDir + '/epicyon.css'):
  74. cssFilename = baseDir + '/epicyon.css'
  75. instanceTitle = \
  76. getConfigParam(baseDir, 'instanceTitle')
  77. infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
  78. searchNickname = getNicknameFromActor(searchHandle)
  79. searchDomain, searchPort = getDomainFromActor(searchHandle)
  80. searchHandle = searchNickname + '@' + searchDomain
  81. searchActor = \
  82. httpPrefix + '://' + searchDomain + '/users/' + searchNickname
  83. infoForm += \
  84. '<center><h1><a href="/users/' + nickname + '/moderation">' + \
  85. translate['Account Information'] + ':</a> <a href="' + searchActor + \
  86. '">' + searchHandle + '</a></h1><br>\n'
  87. infoForm += translate[msgStr1] + '</center><br><br>\n'
  88. proxyType = 'tor'
  89. if not os.path.isfile('/usr/bin/tor'):
  90. proxyType = None
  91. if domain.endswith('.i2p'):
  92. proxyType = None
  93. session = createSession(proxyType)
  94. wordFrequency = {}
  95. domainDict = getPublicPostInfo(session,
  96. baseDir, searchNickname, searchDomain,
  97. proxyType, searchPort,
  98. httpPrefix, debug,
  99. __version__, wordFrequency)
  100. # get a list of any blocked followers
  101. followersList = \
  102. downloadFollowCollection('followers', session,
  103. httpPrefix, searchActor, 1, 5)
  104. blockedFollowers = []
  105. for followerActor in followersList:
  106. followerNickname = getNicknameFromActor(followerActor)
  107. followerDomain, followerPort = getDomainFromActor(followerActor)
  108. followerDomainFull = getFullDomain(followerDomain, followerPort)
  109. if isBlocked(baseDir, nickname, domain,
  110. followerNickname, followerDomainFull):
  111. blockedFollowers.append(followerActor)
  112. # get a list of any blocked following
  113. followingList = \
  114. downloadFollowCollection('following', session,
  115. httpPrefix, searchActor, 1, 5)
  116. blockedFollowing = []
  117. for followingActor in followingList:
  118. followingNickname = getNicknameFromActor(followingActor)
  119. followingDomain, followingPort = getDomainFromActor(followingActor)
  120. followingDomainFull = getFullDomain(followingDomain, followingPort)
  121. if isBlocked(baseDir, nickname, domain,
  122. followingNickname, followingDomainFull):
  123. blockedFollowing.append(followingActor)
  124. infoForm += '<div class="accountInfoDomains">\n'
  125. usersPath = '/users/' + nickname + '/accountinfo'
  126. ctr = 1
  127. for postDomain, blockedPostUrls in domainDict.items():
  128. infoForm += '<a href="' + \
  129. httpPrefix + '://' + postDomain + '">' + postDomain + '</a> '
  130. if isBlockedDomain(baseDir, postDomain):
  131. blockedPostsLinks = ''
  132. urlCtr = 0
  133. for url in blockedPostUrls:
  134. if urlCtr > 0:
  135. blockedPostsLinks += '<br>'
  136. blockedPostsLinks += \
  137. '<a href="' + url + '">' + url + '</a>'
  138. urlCtr += 1
  139. blockedPostsHtml = ''
  140. if blockedPostsLinks:
  141. blockNoStr = 'blockNumber' + str(ctr)
  142. blockedPostsHtml = \
  143. getContentWarningButton(blockNoStr,
  144. translate, blockedPostsLinks)
  145. ctr += 1
  146. infoForm += \
  147. '<a href="' + usersPath + '?unblockdomain=' + postDomain + \
  148. '?handle=' + searchHandle + '">'
  149. infoForm += '<button class="buttonhighlighted"><span>' + \
  150. translate['Unblock'] + '</span></button></a> ' + \
  151. blockedPostsHtml + '\n'
  152. else:
  153. infoForm += \
  154. '<a href="' + usersPath + '?blockdomain=' + postDomain + \
  155. '?handle=' + searchHandle + '">'
  156. if postDomain != domain:
  157. infoForm += '<button class="button"><span>' + \
  158. translate['Block'] + '</span></button>'
  159. infoForm += '</a>\n'
  160. infoForm += '<br>\n'
  161. infoForm += '</div>\n'
  162. if blockedFollowing:
  163. blockedFollowing.sort()
  164. infoForm += '<div class="accountInfoDomains">\n'
  165. infoForm += '<h1>' + translate['Blocked following'] + '</h1>\n'
  166. infoForm += \
  167. '<p>' + \
  168. translate['Receives posts from the following accounts'] + \
  169. ':</p>\n'
  170. for actor in blockedFollowing:
  171. followingNickname = getNicknameFromActor(actor)
  172. followingDomain, followingPort = getDomainFromActor(actor)
  173. followingDomainFull = \
  174. getFullDomain(followingDomain, followingPort)
  175. infoForm += '<a href="' + actor + '">' + \
  176. followingNickname + '@' + followingDomainFull + \
  177. '</a><br><br>\n'
  178. infoForm += '</div>\n'
  179. if blockedFollowers:
  180. blockedFollowers.sort()
  181. infoForm += '<div class="accountInfoDomains">\n'
  182. infoForm += '<h1>' + translate['Blocked followers'] + '</h1>\n'
  183. infoForm += \
  184. '<p>' + \
  185. translate['Sends out posts to the following accounts'] + \
  186. ':</p>\n'
  187. for actor in blockedFollowers:
  188. followerNickname = getNicknameFromActor(actor)
  189. followerDomain, followerPort = getDomainFromActor(actor)
  190. followerDomainFull = getFullDomain(followerDomain, followerPort)
  191. infoForm += '<a href="' + actor + '">' + \
  192. followerNickname + '@' + followerDomainFull + '</a><br><br>\n'
  193. infoForm += '</div>\n'
  194. if wordFrequency:
  195. maxCount = 1
  196. for word, count in wordFrequency.items():
  197. if count > maxCount:
  198. maxCount = count
  199. minimumWordCount = int(maxCount / 2)
  200. if minimumWordCount >= 3:
  201. infoForm += '<div class="accountInfoDomains">\n'
  202. infoForm += '<h1>' + translate['Word frequencies'] + '</h1>\n'
  203. wordSwarm = ''
  204. ctr = 0
  205. for word, count in wordFrequency.items():
  206. if count >= minimumWordCount:
  207. if ctr > 0:
  208. wordSwarm += ' '
  209. if count < maxCount - int(maxCount / 4):
  210. wordSwarm += word
  211. else:
  212. if count != maxCount:
  213. wordSwarm += '<b>' + word + '</b>'
  214. else:
  215. wordSwarm += '<b><i>' + word + '</i></b>'
  216. ctr += 1
  217. infoForm += wordSwarm
  218. infoForm += '</div>\n'
  219. infoForm += htmlFooter()
  220. return infoForm
  221. def htmlModerationInfo(cssCache: {}, translate: {},
  222. baseDir: str, httpPrefix: str,
  223. nickname: str) -> str:
  224. msgStr1 = \
  225. 'These are globally blocked for all accounts on this instance'
  226. msgStr2 = \
  227. 'Any blocks or suspensions made by moderators will be shown here.'
  228. infoForm = ''
  229. cssFilename = baseDir + '/epicyon-profile.css'
  230. if os.path.isfile(baseDir + '/epicyon.css'):
  231. cssFilename = baseDir + '/epicyon.css'
  232. instanceTitle = \
  233. getConfigParam(baseDir, 'instanceTitle')
  234. infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
  235. infoForm += \
  236. '<center><h1><a href="/users/' + nickname + '/moderation">' + \
  237. translate['Moderation Information'] + \
  238. '</a></h1></center><br>'
  239. infoShown = False
  240. accounts = []
  241. for subdir, dirs, files in os.walk(baseDir + '/accounts'):
  242. for acct in dirs:
  243. if '@' not in acct:
  244. continue
  245. if acct.startswith('inbox@'):
  246. continue
  247. elif acct.startswith('news@'):
  248. continue
  249. accounts.append(acct)
  250. break
  251. accounts.sort()
  252. cols = 5
  253. if len(accounts) > 10:
  254. infoForm += '<details><summary><b>' + translate['Show Accounts']
  255. infoForm += '</b></summary>\n'
  256. infoForm += '<div class="container">\n'
  257. infoForm += '<table class="accountsTable">\n'
  258. infoForm += ' <colgroup>\n'
  259. for col in range(cols):
  260. infoForm += ' <col span="1" class="accountsTableCol">\n'
  261. infoForm += ' </colgroup>\n'
  262. infoForm += '<tr>\n'
  263. col = 0
  264. for acct in accounts:
  265. acctNickname = acct.split('@')[0]
  266. accountDir = os.path.join(baseDir + '/accounts', acct)
  267. actorJson = loadJson(accountDir + '.json')
  268. if not actorJson:
  269. continue
  270. actor = actorJson['id']
  271. avatarUrl = ''
  272. ext = ''
  273. if actorJson.get('icon'):
  274. if actorJson['icon'].get('url'):
  275. avatarUrl = actorJson['icon']['url']
  276. if '.' in avatarUrl:
  277. ext = '.' + avatarUrl.split('.')[-1]
  278. acctUrl = \
  279. '/users/' + nickname + '?options=' + actor + ';1;' + \
  280. '/members/' + acctNickname + ext
  281. infoForm += '<td>\n<a href="' + acctUrl + '">'
  282. infoForm += '<img loading="lazy" style="width:90%" '
  283. infoForm += 'src="' + avatarUrl + '" />'
  284. infoForm += '<br><center>'
  285. if isModerator(baseDir, acctNickname):
  286. infoForm += '<b><u>' + acctNickname + '</u></b>'
  287. else:
  288. infoForm += acctNickname
  289. if isEditor(baseDir, acctNickname):
  290. infoForm += ' ✍'
  291. infoForm += '</center></a>\n</td>\n'
  292. col += 1
  293. if col == cols:
  294. # new row of accounts
  295. infoForm += '</tr>\n<tr>\n'
  296. infoForm += '</tr>\n</table>\n'
  297. infoForm += '</div>\n'
  298. if len(accounts) > 10:
  299. infoForm += '</details>\n'
  300. suspendedFilename = baseDir + '/accounts/suspended.txt'
  301. if os.path.isfile(suspendedFilename):
  302. with open(suspendedFilename, "r") as f:
  303. suspendedStr = f.read()
  304. infoForm += '<div class="container">\n'
  305. infoForm += ' <br><b>' + \
  306. translate['Suspended accounts'] + '</b>'
  307. infoForm += ' <br>' + \
  308. translate['These are currently suspended']
  309. infoForm += \
  310. ' <textarea id="message" ' + \
  311. 'name="suspended" style="height:200px" spellcheck="false">' + \
  312. suspendedStr + '</textarea>\n'
  313. infoForm += '</div>\n'
  314. infoShown = True
  315. blockingFilename = baseDir + '/accounts/blocking.txt'
  316. if os.path.isfile(blockingFilename):
  317. with open(blockingFilename, "r") as f:
  318. blockedStr = f.read()
  319. infoForm += '<div class="container">\n'
  320. infoForm += \
  321. ' <br><b>' + \
  322. translate['Blocked accounts and hashtags'] + '</b>'
  323. infoForm += \
  324. ' <br>' + \
  325. translate[msgStr1]
  326. infoForm += \
  327. ' <textarea id="message" ' + \
  328. 'name="blocked" style="height:700px" spellcheck="false">' + \
  329. blockedStr + '</textarea>\n'
  330. infoForm += '</div>\n'
  331. infoShown = True
  332. filtersFilename = baseDir + '/accounts/filters.txt'
  333. if os.path.isfile(filtersFilename):
  334. with open(filtersFilename, "r") as f:
  335. filteredStr = f.read()
  336. infoForm += '<div class="container">\n'
  337. infoForm += \
  338. ' <br><b>' + \
  339. translate['Filtered words'] + '</b>'
  340. infoForm += \
  341. ' <textarea id="message" ' + \
  342. 'name="filtered" style="height:700px" spellcheck="true">' + \
  343. filteredStr + '</textarea>\n'
  344. infoForm += '</div>\n'
  345. infoShown = True
  346. if not infoShown:
  347. infoForm += \
  348. '<center><p>' + \
  349. translate[msgStr2] + \
  350. '</p></center>\n'
  351. infoForm += htmlFooter()
  352. return infoForm