epicyon/webapp_moderation.py

415 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-12-04 17:18:32 +00:00
from utils import isArtist
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,
2021-12-25 20:28:06 +00:00
recentPostsCache: {}, max_recent_posts: int,
2020-11-10 10:25:21 +00:00
translate: {}, pageNumber: int, itemsPerPage: int,
2021-12-25 22:17:49 +00:00
session, base_dir: str, wfRequest: {}, person_cache: {},
2020-11-10 10:25:21 +00:00
nickname: str, domain: str, port: int, inboxJson: {},
2021-12-25 21:29:53 +00:00
allow_deletion: bool,
2021-12-25 20:34:38 +00:00
http_prefix: str, project_version: str,
2021-12-25 17:15:52 +00:00
yt_replace_domain: str,
2021-12-25 20:55:47 +00:00
twitter_replacement_domain: str,
2021-12-25 20:06:27 +00:00
show_published_date_only: bool,
2021-12-25 20:14:45 +00:00
newswire: {}, positive_voting: bool,
2021-12-25 19:34:20 +00:00
show_publish_as_icon: bool,
2021-12-25 19:31:24 +00:00
full_width_tl_button_header: bool,
2021-12-25 19:19:14 +00:00
icons_as_buttons: bool,
2021-12-25 19:09:03 +00:00
rss_icon_at_top: bool,
2021-12-25 19:00:00 +00:00
publish_button_at_top: bool,
2020-12-21 23:09:00 +00:00
authorized: bool, moderationActionStr: str,
theme: str, peertubeInstances: [],
2021-12-25 18:54:50 +00:00
allow_local_network_access: bool,
textModeBanner: str,
2021-12-25 23:03:28 +00:00
accessKeys: {}, system_language: str,
2021-12-25 18:23:12 +00:00
max_like_count: int,
2021-12-25 18:05:01 +00:00
shared_items_federated_domains: [],
2021-12-25 23:03:28 +00:00
signing_priv_key_pem: str,
2021-12-25 18:12:13 +00:00
CWlists: {}, lists_enabled: 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
"""
2021-12-25 16:17:53 +00:00
artist = isArtist(base_dir, nickname)
2020-11-10 10:25:21 +00:00
return htmlTimeline(cssCache, defaultTimeline,
2021-12-25 20:28:06 +00:00
recentPostsCache, max_recent_posts,
2020-11-10 10:25:21 +00:00
translate, pageNumber,
2021-12-25 16:17:53 +00:00
itemsPerPage, session, base_dir,
2021-12-25 22:17:49 +00:00
wfRequest, person_cache,
2020-11-10 10:25:21 +00:00
nickname, domain, port, inboxJson, 'moderation',
2021-12-25 21:29:53 +00:00
allow_deletion, http_prefix,
2021-12-25 20:34:38 +00:00
project_version, True, False,
2021-12-25 17:15:52 +00:00
yt_replace_domain,
2021-12-25 20:55:47 +00:00
twitter_replacement_domain,
2021-12-25 20:06:27 +00:00
show_published_date_only,
2021-12-25 20:14:45 +00:00
newswire, False, False, artist, positive_voting,
2021-12-25 19:34:20 +00:00
show_publish_as_icon,
2021-12-25 19:31:24 +00:00
full_width_tl_button_header,
2021-12-25 19:19:14 +00:00
icons_as_buttons, rss_icon_at_top,
publish_button_at_top,
2020-12-23 23:59:49 +00:00
authorized, moderationActionStr, theme,
2021-12-25 18:54:50 +00:00
peertubeInstances, allow_local_network_access,
2021-12-25 23:03:28 +00:00
textModeBanner, accessKeys, system_language,
2021-12-25 18:23:12 +00:00
max_like_count, shared_items_federated_domains,
2021-12-25 23:03:28 +00:00
signing_priv_key_pem, CWlists, lists_enabled)
2020-11-10 10:25:21 +00:00
2020-12-09 22:55:15 +00:00
def htmlAccountInfo(cssCache: {}, translate: {},
2021-12-25 17:09:22 +00:00
base_dir: str, http_prefix: str,
2020-12-09 22:55:15 +00:00
nickname: str, domain: str, port: int,
searchHandle: str, debug: bool,
2021-12-25 23:03:28 +00:00
system_language: str, signing_priv_key_pem: 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-12-25 23:03:28 +00:00
signing_priv_key_pem = None
2020-12-09 22:55:15 +00:00
msgStr1 = 'This account interacts with the following instances'
infoForm = ''
2021-12-25 16:17:53 +00:00
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
2020-12-09 22:55:15 +00:00
2021-01-11 19:46:21 +00:00
instanceTitle = \
2021-12-25 16:17:53 +00:00
getConfigParam(base_dir, 'instanceTitle')
infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
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-12-25 17:09:22 +00:00
localActorUrl(http_prefix, 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
2021-12-25 21:09:22 +00:00
proxy_type = 'tor'
if not os.path.isfile('/usr/bin/tor'):
2021-12-25 21:09:22 +00:00
proxy_type = None
if domain.endswith('.i2p'):
2021-12-25 21:09:22 +00:00
proxy_type = None
2021-01-10 23:06:38 +00:00
2021-12-25 21:09:22 +00:00
session = createSession(proxy_type)
2021-01-10 23:06:38 +00:00
wordFrequency = {}
2021-09-15 14:05:08 +00:00
originDomain = None
2021-01-10 23:06:38 +00:00
domainDict = getPublicPostInfo(session,
2021-12-25 16:17:53 +00:00
base_dir, searchNickname, searchDomain,
2021-09-15 14:05:08 +00:00
originDomain,
2021-12-25 21:09:22 +00:00
proxy_type, searchPort,
2021-12-25 17:09:22 +00:00
http_prefix, debug,
2021-12-25 23:03:28 +00:00
__version__, wordFrequency, system_language,
signing_priv_key_pem)
2021-01-10 23:06:38 +00:00
# get a list of any blocked followers
followersList = \
2021-12-25 23:03:28 +00:00
downloadFollowCollection(signing_priv_key_pem,
'followers', session,
2021-12-25 17:09:22 +00:00
http_prefix, 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)
2021-12-25 16:17:53 +00:00
if isBlocked(base_dir, nickname, domain,
2021-01-10 23:06:38 +00:00
followerNickname, followerDomainFull):
blockedFollowers.append(followerActor)
# get a list of any blocked following
followingList = \
2021-12-25 23:03:28 +00:00
downloadFollowCollection(signing_priv_key_pem,
2021-08-31 16:21:37 +00:00
'following', session,
2021-12-25 17:09:22 +00:00
http_prefix, searchActor, 1, 5, debug)
blockedFollowing = []
for followingActor in followingList:
followingNickname = getNicknameFromActor(followingActor)
followingDomain, followingPort = getDomainFromActor(followingActor)
followingDomainFull = getFullDomain(followingDomain, followingPort)
2021-12-25 16:17:53 +00:00
if isBlocked(base_dir, 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-12-25 17:09:22 +00:00
http_prefix + '://' + postDomain + '" ' + \
2021-07-23 13:50:32 +00:00
'target="_blank" rel="nofollow noopener noreferrer">' + \
postDomain + '</a> '
2021-12-25 16:17:53 +00:00
if isBlockedDomain(base_dir, 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: {},
2021-12-25 17:09:22 +00:00
base_dir: str, http_prefix: str,
2020-12-09 19:17:42 +00:00
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 = ''
2021-12-25 16:17:53 +00:00
cssFilename = base_dir + '/epicyon-profile.css'
if os.path.isfile(base_dir + '/epicyon.css'):
cssFilename = base_dir + '/epicyon.css'
2020-11-10 10:25:21 +00:00
2021-01-11 19:46:21 +00:00
instanceTitle = \
2021-12-25 16:17:53 +00:00
getConfigParam(base_dir, 'instanceTitle')
infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None)
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 = []
2021-12-25 16:17:53 +00:00
for subdir, dirs, files in os.walk(base_dir + '/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]
2021-12-25 16:17:53 +00:00
accountDir = os.path.join(base_dir + '/accounts', acct)
2020-12-20 11:31:29 +00:00
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>'
2021-12-25 16:17:53 +00:00
if isModerator(base_dir, acctNickname):
2020-12-20 12:38:47 +00:00
infoForm += '<b><u>' + acctNickname + '</u></b>'
else:
infoForm += acctNickname
2021-12-25 16:17:53 +00:00
if isEditor(base_dir, 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'
2021-12-25 16:17:53 +00:00
suspendedFilename = base_dir + '/accounts/suspended.txt'
2020-11-12 17:05:38 +00:00
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
2021-12-25 16:17:53 +00:00
blockingFilename = base_dir + '/accounts/blocking.txt'
2020-11-12 17:05:38 +00:00
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
2021-12-25 16:17:53 +00:00
filtersFilename = base_dir + '/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