epicyon/webapp_moderation.py

409 lines
16 KiB
Python
Raw Normal View History

2020-11-10 10:25:21 +00:00
__filename__ = "webapp_moderation.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
2021-01-26 10:07:42 +00:00
__version__ = "1.2.0"
2020-11-10 10:25:21 +00:00
__maintainer__ = "Bob Mottram"
2021-09-10 16:14:50 +00:00
__email__ = "bob@libreserver.org"
2020-11-10 10:25:21 +00:00
__status__ = "Production"
2021-06-26 11:27:14 +00:00
__module_group__ = "Moderation"
2020-11-10 10:25:21 +00:00
import os
2021-06-25 18:02:05 +00:00
from utils import isAccountDir
2021-01-10 23:06:38 +00:00
from utils import getFullDomain
2020-12-20 12:34:16 +00:00
from utils import isEditor
from utils import loadJson
2020-12-09 22:55:15 +00:00
from utils import getNicknameFromActor
from utils import getDomainFromActor
2021-01-11 19:46:21 +00:00
from utils import getConfigParam
2021-08-14 11:13:39 +00:00
from utils import localActorUrl
from posts import downloadFollowCollection
from posts import getPublicPostInfo
2020-12-20 12:09:29 +00:00
from posts import isModerator
2020-11-10 10:25:21 +00:00
from webapp_timeline import htmlTimeline
# from webapp_utils import getPersonAvatarUrl
from webapp_utils import getContentWarningButton
2020-11-12 17:05:38 +00:00
from webapp_utils import htmlHeaderWithExternalStyle
2020-11-10 10:25:21 +00:00
from webapp_utils import htmlFooter
2020-12-09 22:55:15 +00:00
from blocking import isBlockedDomain
2021-01-10 23:06:38 +00:00
from blocking import isBlocked
from session import createSession
2020-11-10 10:25:21 +00:00
def htmlModeration(cssCache: {}, defaultTimeline: str,
recentPostsCache: {}, maxRecentPosts: int,
translate: {}, pageNumber: int, itemsPerPage: int,
session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool,
httpPrefix: str, projectVersion: str,
YTReplacementDomain: str,
2021-09-18 17:08:14 +00:00
twitterReplacementDomain: str,
2020-11-10 10:25:21 +00:00
showPublishedDateOnly: bool,
newswire: {}, positiveVoting: bool,
showPublishAsIcon: bool,
fullWidthTimelineButtonHeader: bool,
iconsAsButtons: bool,
rssIconAtTop: bool,
publishButtonAtTop: bool,
2020-12-21 23:09:00 +00:00
authorized: bool, moderationActionStr: str,
theme: str, peertubeInstances: [],
2021-02-05 19:14:27 +00:00
allowLocalNetworkAccess: bool,
textModeBanner: str,
accessKeys: {}, systemLanguage: str,
maxLikeCount: int,
sharedItemsFederatedDomains: [],
2021-10-21 13:08:21 +00:00
signingPrivateKeyPem: str,
CWlists: {}, listsEnabled: str) -> str:
2020-11-10 10:25:21 +00:00
"""Show the moderation feed as html
This is what you see when selecting the "mod" timeline
"""
return htmlTimeline(cssCache, defaultTimeline,
recentPostsCache, maxRecentPosts,
translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'moderation',
allowDeletion, httpPrefix, projectVersion, True, False,
2021-09-18 17:08:14 +00:00
YTReplacementDomain,
twitterReplacementDomain,
showPublishedDateOnly,
2020-11-10 10:25:21 +00:00
newswire, False, False, positiveVoting,
showPublishAsIcon, fullWidthTimelineButtonHeader,
iconsAsButtons, rssIconAtTop, publishButtonAtTop,
2020-12-23 23:59:49 +00:00
authorized, moderationActionStr, theme,
2021-02-05 19:14:27 +00:00
peertubeInstances, allowLocalNetworkAccess,
textModeBanner, accessKeys, systemLanguage,
maxLikeCount, sharedItemsFederatedDomains,
signingPrivateKeyPem, CWlists, listsEnabled)
2020-11-10 10:25:21 +00:00
2020-12-09 22:55:15 +00:00
def htmlAccountInfo(cssCache: {}, translate: {},
baseDir: str, httpPrefix: str,
nickname: str, domain: str, port: int,
searchHandle: str, debug: bool,
systemLanguage: str, signingPrivateKeyPem: str) -> str:
2020-12-09 22:55:15 +00:00
"""Shows which domains a search handle interacts with.
This screen is shown if a moderator enters a handle and selects info
on the moderation screen
"""
2021-09-15 14:05:08 +00:00
signingPrivateKeyPem = None
2020-12-09 22:55:15 +00:00
msgStr1 = 'This account interacts with the following instances'
infoForm = ''
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-12-09 22:55:15 +00:00
searchNickname = getNicknameFromActor(searchHandle)
searchDomain, searchPort = getDomainFromActor(searchHandle)
2020-12-10 10:21:54 +00:00
searchHandle = searchNickname + '@' + searchDomain
2021-01-10 23:06:38 +00:00
searchActor = \
2021-08-14 11:13:39 +00:00
localActorUrl(httpPrefix, searchNickname, searchDomain)
2020-12-09 22:55:15 +00:00
infoForm += \
2020-12-10 10:21:54 +00:00
'<center><h1><a href="/users/' + nickname + '/moderation">' + \
2021-01-10 23:06:38 +00:00
translate['Account Information'] + ':</a> <a href="' + searchActor + \
'">' + searchHandle + '</a></h1><br>\n'
2020-12-09 22:55:15 +00:00
2021-01-10 23:06:38 +00:00
infoForm += translate[msgStr1] + '</center><br><br>\n'
2020-12-09 22:55:15 +00:00
proxyType = 'tor'
if not os.path.isfile('/usr/bin/tor'):
proxyType = None
if domain.endswith('.i2p'):
proxyType = None
2021-01-10 23:06:38 +00:00
session = createSession(proxyType)
wordFrequency = {}
2021-09-15 14:05:08 +00:00
originDomain = None
2021-01-10 23:06:38 +00:00
domainDict = getPublicPostInfo(session,
baseDir, searchNickname, searchDomain,
2021-09-15 14:05:08 +00:00
originDomain,
proxyType, searchPort,
httpPrefix, debug,
__version__, wordFrequency, systemLanguage,
signingPrivateKeyPem)
2021-01-10 23:06:38 +00:00
# get a list of any blocked followers
followersList = \
downloadFollowCollection(signingPrivateKeyPem,
'followers', session,
httpPrefix, searchActor, 1, 5, debug)
2021-01-10 23:06:38 +00:00
blockedFollowers = []
for followerActor in followersList:
followerNickname = getNicknameFromActor(followerActor)
followerDomain, followerPort = getDomainFromActor(followerActor)
followerDomainFull = getFullDomain(followerDomain, followerPort)
if isBlocked(baseDir, nickname, domain,
followerNickname, followerDomainFull):
blockedFollowers.append(followerActor)
# get a list of any blocked following
followingList = \
2021-08-31 16:21:37 +00:00
downloadFollowCollection(signingPrivateKeyPem,
'following', session,
httpPrefix, searchActor, 1, 5, debug)
blockedFollowing = []
for followingActor in followingList:
followingNickname = getNicknameFromActor(followingActor)
followingDomain, followingPort = getDomainFromActor(followingActor)
followingDomainFull = getFullDomain(followingDomain, followingPort)
if isBlocked(baseDir, nickname, domain,
followingNickname, followingDomainFull):
blockedFollowing.append(followingActor)
2021-01-10 23:06:38 +00:00
infoForm += '<div class="accountInfoDomains">\n'
2020-12-09 22:55:15 +00:00
usersPath = '/users/' + nickname + '/accountinfo'
ctr = 1
2020-12-16 17:09:08 +00:00
for postDomain, blockedPostUrls in domainDict.items():
2020-12-09 22:55:15 +00:00
infoForm += '<a href="' + \
2021-07-23 13:50:32 +00:00
httpPrefix + '://' + postDomain + '" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \
postDomain + '</a> '
2020-12-09 23:08:00 +00:00
if isBlockedDomain(baseDir, postDomain):
blockedPostsLinks = ''
2020-12-16 17:24:56 +00:00
urlCtr = 0
2020-12-16 17:09:08 +00:00
for url in blockedPostUrls:
2020-12-16 17:24:56 +00:00
if urlCtr > 0:
blockedPostsLinks += '<br>'
blockedPostsLinks += \
2021-07-23 13:50:32 +00:00
'<a href="' + url + '" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \
url + '</a>'
2020-12-16 17:24:56 +00:00
urlCtr += 1
2020-12-16 16:46:36 +00:00
blockedPostsHtml = ''
if blockedPostsLinks:
blockNoStr = 'blockNumber' + str(ctr)
2020-12-16 16:46:36 +00:00
blockedPostsHtml = \
getContentWarningButton(blockNoStr,
2020-12-16 16:46:36 +00:00
translate, blockedPostsLinks)
ctr += 1
2020-12-09 22:55:15 +00:00
infoForm += \
2020-12-10 10:21:54 +00:00
'<a href="' + usersPath + '?unblockdomain=' + postDomain + \
'?handle=' + searchHandle + '">'
2020-12-09 22:55:15 +00:00
infoForm += '<button class="buttonhighlighted"><span>' + \
translate['Unblock'] + '</span></button></a> ' + \
2021-01-10 23:06:38 +00:00
blockedPostsHtml + '\n'
2020-12-09 22:55:15 +00:00
else:
infoForm += \
2020-12-10 10:21:54 +00:00
'<a href="' + usersPath + '?blockdomain=' + postDomain + \
'?handle=' + searchHandle + '">'
if postDomain != domain:
infoForm += '<button class="button"><span>' + \
translate['Block'] + '</span></button>'
2021-01-10 23:06:38 +00:00
infoForm += '</a>\n'
infoForm += '<br>\n'
infoForm += '</div>\n'
if blockedFollowing:
blockedFollowing.sort()
infoForm += '<div class="accountInfoDomains">\n'
infoForm += '<h1>' + translate['Blocked following'] + '</h1>\n'
infoForm += \
'<p>' + \
translate['Receives posts from the following accounts'] + \
2021-01-11 20:46:57 +00:00
':</p>\n'
for actor in blockedFollowing:
followingNickname = getNicknameFromActor(actor)
followingDomain, followingPort = getDomainFromActor(actor)
followingDomainFull = \
getFullDomain(followingDomain, followingPort)
2021-07-23 13:50:32 +00:00
infoForm += '<a href="' + actor + '" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \
followingNickname + '@' + followingDomainFull + \
'</a><br><br>\n'
infoForm += '</div>\n'
2021-01-10 23:06:38 +00:00
if blockedFollowers:
blockedFollowers.sort()
2021-01-10 23:17:29 +00:00
infoForm += '<div class="accountInfoDomains">\n'
2021-01-10 23:19:06 +00:00
infoForm += '<h1>' + translate['Blocked followers'] + '</h1>\n'
infoForm += \
'<p>' + \
translate['Sends out posts to the following accounts'] + \
2021-01-11 20:46:57 +00:00
':</p>\n'
2021-01-10 23:06:38 +00:00
for actor in blockedFollowers:
2021-01-10 23:16:10 +00:00
followerNickname = getNicknameFromActor(actor)
followerDomain, followerPort = getDomainFromActor(actor)
followerDomainFull = getFullDomain(followerDomain, followerPort)
2021-07-23 13:50:32 +00:00
infoForm += '<a href="' + actor + '" ' + \
'target="_blank" rel="nofollow noopener noreferrer">' + \
2021-01-10 23:16:10 +00:00
followerNickname + '@' + followerDomainFull + '</a><br><br>\n'
2021-01-10 23:06:38 +00:00
infoForm += '</div>\n'
2020-12-09 22:55:15 +00:00
2021-01-11 13:42:47 +00:00
if wordFrequency:
maxCount = 1
for word, count in wordFrequency.items():
if count > maxCount:
maxCount = count
minimumWordCount = int(maxCount / 2)
if minimumWordCount >= 3:
infoForm += '<div class="accountInfoDomains">\n'
infoForm += '<h1>' + translate['Word frequencies'] + '</h1>\n'
wordSwarm = ''
ctr = 0
for word, count in wordFrequency.items():
if count >= minimumWordCount:
if ctr > 0:
wordSwarm += ' '
if count < maxCount - int(maxCount / 4):
wordSwarm += word
else:
if count != maxCount:
wordSwarm += '<b>' + word + '</b>'
else:
wordSwarm += '<b><i>' + word + '</i></b>'
ctr += 1
infoForm += wordSwarm
infoForm += '</div>\n'
2020-12-09 22:55:15 +00:00
infoForm += htmlFooter()
return infoForm
2020-11-10 10:25:21 +00:00
def htmlModerationInfo(cssCache: {}, translate: {},
2020-12-09 19:17:42 +00:00
baseDir: str, httpPrefix: str,
nickname: str) -> str:
2020-11-10 10:25:21 +00:00
msgStr1 = \
'These are globally blocked for all accounts on this instance'
msgStr2 = \
'Any blocks or suspensions made by moderators will be shown here.'
2020-12-09 22:55:15 +00:00
2020-11-10 10:25:21 +00:00
infoForm = ''
cssFilename = baseDir + '/epicyon-profile.css'
if os.path.isfile(baseDir + '/epicyon.css'):
cssFilename = baseDir + '/epicyon.css'
2021-01-11 19:46:21 +00:00
instanceTitle = \
getConfigParam(baseDir, 'instanceTitle')
infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle)
2020-11-10 10:25:21 +00:00
2020-11-12 17:05:38 +00:00
infoForm += \
2020-12-09 19:17:42 +00:00
'<center><h1><a href="/users/' + nickname + '/moderation">' + \
2020-11-12 17:05:38 +00:00
translate['Moderation Information'] + \
2020-12-09 22:55:15 +00:00
'</a></h1></center><br>'
2020-11-10 10:25:21 +00:00
2020-11-12 17:05:38 +00:00
infoShown = False
2020-12-20 11:31:29 +00:00
accounts = []
for subdir, dirs, files in os.walk(baseDir + '/accounts'):
for acct in dirs:
2021-06-25 18:02:05 +00:00
if not isAccountDir(acct):
continue
2020-12-20 11:31:29 +00:00
accounts.append(acct)
break
2020-12-20 11:31:29 +00:00
accounts.sort()
cols = 5
if len(accounts) > 10:
infoForm += '<details><summary><b>' + translate['Show Accounts']
infoForm += '</b></summary>\n'
infoForm += '<div class="container">\n'
infoForm += '<table class="accountsTable">\n'
infoForm += ' <colgroup>\n'
for col in range(cols):
infoForm += ' <col span="1" class="accountsTableCol">\n'
infoForm += ' </colgroup>\n'
infoForm += '<tr>\n'
2020-12-20 11:31:29 +00:00
col = 0
for acct in accounts:
acctNickname = acct.split('@')[0]
accountDir = os.path.join(baseDir + '/accounts', acct)
actorJson = loadJson(accountDir + '.json')
if not actorJson:
continue
actor = actorJson['id']
avatarUrl = ''
2020-12-20 13:28:33 +00:00
ext = ''
2020-12-20 11:31:29 +00:00
if actorJson.get('icon'):
if actorJson['icon'].get('url'):
avatarUrl = actorJson['icon']['url']
2020-12-20 13:28:33 +00:00
if '.' in avatarUrl:
2020-12-20 13:30:55 +00:00
ext = '.' + avatarUrl.split('.')[-1]
2020-12-20 11:31:29 +00:00
acctUrl = \
'/users/' + nickname + '?options=' + actor + ';1;' + \
2020-12-20 13:28:33 +00:00
'/members/' + acctNickname + ext
2020-12-20 11:31:29 +00:00
infoForm += '<td>\n<a href="' + acctUrl + '">'
infoForm += '<img loading="lazy" style="width:90%" '
infoForm += 'src="' + avatarUrl + '" />'
2020-12-20 12:09:29 +00:00
infoForm += '<br><center>'
2020-12-20 12:43:22 +00:00
if isModerator(baseDir, acctNickname):
2020-12-20 12:38:47 +00:00
infoForm += '<b><u>' + acctNickname + '</u></b>'
else:
infoForm += acctNickname
2020-12-20 12:34:16 +00:00
if isEditor(baseDir, acctNickname):
2020-12-20 12:35:56 +00:00
infoForm += ''
2020-12-20 11:31:29 +00:00
infoForm += '</center></a>\n</td>\n'
col += 1
if col == cols:
# new row of accounts
infoForm += '</tr>\n<tr>\n'
infoForm += '</tr>\n</table>\n'
infoForm += '</div>\n'
if len(accounts) > 10:
infoForm += '</details>\n'
2020-11-12 17:05:38 +00:00
suspendedFilename = baseDir + '/accounts/suspended.txt'
if os.path.isfile(suspendedFilename):
2021-07-13 14:40:49 +00:00
with open(suspendedFilename, 'r') as f:
2020-11-12 17:05:38 +00:00
suspendedStr = f.read()
infoForm += '<div class="container">\n'
2020-11-12 17:05:38 +00:00
infoForm += ' <br><b>' + \
translate['Suspended accounts'] + '</b>'
infoForm += ' <br>' + \
translate['These are currently suspended']
infoForm += \
' <textarea id="message" ' + \
2021-02-28 14:26:04 +00:00
'name="suspended" style="height:200px" spellcheck="false">' + \
suspendedStr + '</textarea>\n'
infoForm += '</div>\n'
2020-11-12 17:05:38 +00:00
infoShown = True
2020-11-10 10:25:21 +00:00
2020-11-12 17:05:38 +00:00
blockingFilename = baseDir + '/accounts/blocking.txt'
if os.path.isfile(blockingFilename):
2021-07-13 14:40:49 +00:00
with open(blockingFilename, 'r') as f:
2020-11-12 17:05:38 +00:00
blockedStr = f.read()
infoForm += '<div class="container">\n'
2020-11-12 17:05:38 +00:00
infoForm += \
' <br><b>' + \
translate['Blocked accounts and hashtags'] + '</b>'
2020-11-10 10:25:21 +00:00
infoForm += \
2020-11-12 17:05:38 +00:00
' <br>' + \
translate[msgStr1]
infoForm += \
' <textarea id="message" ' + \
2021-02-28 14:26:04 +00:00
'name="blocked" style="height:700px" spellcheck="false">' + \
blockedStr + '</textarea>\n'
infoForm += '</div>\n'
2020-11-12 17:05:38 +00:00
infoShown = True
filtersFilename = baseDir + '/accounts/filters.txt'
if os.path.isfile(filtersFilename):
2021-07-13 14:40:49 +00:00
with open(filtersFilename, 'r') as f:
filteredStr = f.read()
infoForm += '<div class="container">\n'
infoForm += \
' <br><b>' + \
translate['Filtered words'] + '</b>'
infoForm += \
' <textarea id="message" ' + \
2021-02-28 14:26:04 +00:00
'name="filtered" style="height:700px" spellcheck="true">' + \
filteredStr + '</textarea>\n'
infoForm += '</div>\n'
infoShown = True
2020-11-12 17:05:38 +00:00
if not infoShown:
infoForm += \
'<center><p>' + \
translate[msgStr2] + \
'</p></center>\n'
2020-11-12 17:05:38 +00:00
infoForm += htmlFooter()
2020-11-10 10:25:21 +00:00
return infoForm