__filename__ = "webapp_moderation.py" __author__ = "Bob Mottram" __license__ = "AGPL3+" __version__ = "1.2.0" __maintainer__ = "Bob Mottram" __email__ = "bob@libreserver.org" __status__ = "Production" __module_group__ = "Moderation" import os from utils import isArtist from utils import isAccountDir from utils import getFullDomain from utils import isEditor from utils import loadJson from utils import getNicknameFromActor from utils import getDomainFromActor from utils import getConfigParam from utils import local_actor_url from posts import downloadFollowCollection from posts import getPublicPostInfo from posts import isModerator from webapp_timeline import htmlTimeline # from webapp_utils import getPersonAvatarUrl from webapp_utils import getContentWarningButton from webapp_utils import htmlHeaderWithExternalStyle from webapp_utils import htmlFooter from blocking import isBlockedDomain from blocking import isBlocked from session import createSession def htmlModeration(cssCache: {}, defaultTimeline: str, recentPostsCache: {}, max_recent_posts: int, translate: {}, pageNumber: int, itemsPerPage: int, session, base_dir: str, wfRequest: {}, person_cache: {}, nickname: str, domain: str, port: int, inboxJson: {}, allow_deletion: bool, http_prefix: str, project_version: str, yt_replace_domain: str, twitter_replacement_domain: str, show_published_date_only: bool, newswire: {}, positive_voting: bool, show_publish_as_icon: bool, full_width_tl_button_header: bool, icons_as_buttons: bool, rss_icon_at_top: bool, publish_button_at_top: bool, authorized: bool, moderationActionStr: str, theme: str, peertube_instances: [], allow_local_network_access: bool, text_mode_banner: str, accessKeys: {}, system_language: str, max_like_count: int, shared_items_federated_domains: [], signing_priv_key_pem: str, cw_lists: {}, lists_enabled: str) -> str: """Show the moderation feed as html This is what you see when selecting the "mod" timeline """ artist = isArtist(base_dir, nickname) return htmlTimeline(cssCache, defaultTimeline, recentPostsCache, max_recent_posts, translate, pageNumber, itemsPerPage, session, base_dir, wfRequest, person_cache, nickname, domain, port, inboxJson, 'moderation', allow_deletion, http_prefix, project_version, True, False, yt_replace_domain, twitter_replacement_domain, show_published_date_only, newswire, False, False, artist, positive_voting, show_publish_as_icon, full_width_tl_button_header, icons_as_buttons, rss_icon_at_top, publish_button_at_top, authorized, moderationActionStr, theme, peertube_instances, allow_local_network_access, text_mode_banner, accessKeys, system_language, max_like_count, shared_items_federated_domains, signing_priv_key_pem, cw_lists, lists_enabled) def htmlAccountInfo(cssCache: {}, translate: {}, base_dir: str, http_prefix: str, nickname: str, domain: str, port: int, searchHandle: str, debug: bool, system_language: str, signing_priv_key_pem: str) -> str: """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 """ signing_priv_key_pem = None msgStr1 = 'This account interacts with the following instances' infoForm = '' cssFilename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): cssFilename = base_dir + '/epicyon.css' instanceTitle = \ getConfigParam(base_dir, 'instanceTitle') infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None) searchNickname = getNicknameFromActor(searchHandle) searchDomain, searchPort = getDomainFromActor(searchHandle) searchHandle = searchNickname + '@' + searchDomain searchActor = \ local_actor_url(http_prefix, searchNickname, searchDomain) infoForm += \ '<center><h1><a href="/users/' + nickname + '/moderation">' + \ translate['Account Information'] + ':</a> <a href="' + searchActor + \ '">' + searchHandle + '</a></h1><br>\n' infoForm += translate[msgStr1] + '</center><br><br>\n' proxy_type = 'tor' if not os.path.isfile('/usr/bin/tor'): proxy_type = None if domain.endswith('.i2p'): proxy_type = None session = createSession(proxy_type) wordFrequency = {} originDomain = None domainDict = getPublicPostInfo(session, base_dir, searchNickname, searchDomain, originDomain, proxy_type, searchPort, http_prefix, debug, __version__, wordFrequency, system_language, signing_priv_key_pem) # get a list of any blocked followers followersList = \ downloadFollowCollection(signing_priv_key_pem, 'followers', session, http_prefix, searchActor, 1, 5, debug) blockedFollowers = [] for followerActor in followersList: followerNickname = getNicknameFromActor(followerActor) followerDomain, followerPort = getDomainFromActor(followerActor) followerDomainFull = getFullDomain(followerDomain, followerPort) if isBlocked(base_dir, nickname, domain, followerNickname, followerDomainFull): blockedFollowers.append(followerActor) # get a list of any blocked following followingList = \ downloadFollowCollection(signing_priv_key_pem, 'following', session, http_prefix, searchActor, 1, 5, debug) blockedFollowing = [] for followingActor in followingList: followingNickname = getNicknameFromActor(followingActor) followingDomain, followingPort = getDomainFromActor(followingActor) followingDomainFull = getFullDomain(followingDomain, followingPort) if isBlocked(base_dir, nickname, domain, followingNickname, followingDomainFull): blockedFollowing.append(followingActor) infoForm += '<div class="accountInfoDomains">\n' usersPath = '/users/' + nickname + '/accountinfo' ctr = 1 for postDomain, blockedPostUrls in domainDict.items(): infoForm += '<a href="' + \ http_prefix + '://' + postDomain + '" ' + \ 'target="_blank" rel="nofollow noopener noreferrer">' + \ postDomain + '</a> ' if isBlockedDomain(base_dir, postDomain): blockedPostsLinks = '' urlCtr = 0 for url in blockedPostUrls: if urlCtr > 0: blockedPostsLinks += '<br>' blockedPostsLinks += \ '<a href="' + url + '" ' + \ 'target="_blank" rel="nofollow noopener noreferrer">' + \ url + '</a>' urlCtr += 1 blockedPostsHtml = '' if blockedPostsLinks: blockNoStr = 'blockNumber' + str(ctr) blockedPostsHtml = \ getContentWarningButton(blockNoStr, translate, blockedPostsLinks) ctr += 1 infoForm += \ '<a href="' + usersPath + '?unblockdomain=' + postDomain + \ '?handle=' + searchHandle + '">' infoForm += '<button class="buttonhighlighted"><span>' + \ translate['Unblock'] + '</span></button></a> ' + \ blockedPostsHtml + '\n' else: infoForm += \ '<a href="' + usersPath + '?blockdomain=' + postDomain + \ '?handle=' + searchHandle + '">' if postDomain != domain: infoForm += '<button class="button"><span>' + \ translate['Block'] + '</span></button>' 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'] + \ ':</p>\n' for actor in blockedFollowing: followingNickname = getNicknameFromActor(actor) followingDomain, followingPort = getDomainFromActor(actor) followingDomainFull = \ getFullDomain(followingDomain, followingPort) infoForm += '<a href="' + actor + '" ' + \ 'target="_blank" rel="nofollow noopener noreferrer">' + \ followingNickname + '@' + followingDomainFull + \ '</a><br><br>\n' infoForm += '</div>\n' if blockedFollowers: blockedFollowers.sort() infoForm += '<div class="accountInfoDomains">\n' infoForm += '<h1>' + translate['Blocked followers'] + '</h1>\n' infoForm += \ '<p>' + \ translate['Sends out posts to the following accounts'] + \ ':</p>\n' for actor in blockedFollowers: followerNickname = getNicknameFromActor(actor) followerDomain, followerPort = getDomainFromActor(actor) followerDomainFull = getFullDomain(followerDomain, followerPort) infoForm += '<a href="' + actor + '" ' + \ 'target="_blank" rel="nofollow noopener noreferrer">' + \ followerNickname + '@' + followerDomainFull + '</a><br><br>\n' infoForm += '</div>\n' 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' infoForm += htmlFooter() return infoForm def htmlModerationInfo(cssCache: {}, translate: {}, base_dir: str, http_prefix: str, nickname: str) -> str: msgStr1 = \ 'These are globally blocked for all accounts on this instance' msgStr2 = \ 'Any blocks or suspensions made by moderators will be shown here.' infoForm = '' cssFilename = base_dir + '/epicyon-profile.css' if os.path.isfile(base_dir + '/epicyon.css'): cssFilename = base_dir + '/epicyon.css' instanceTitle = \ getConfigParam(base_dir, 'instanceTitle') infoForm = htmlHeaderWithExternalStyle(cssFilename, instanceTitle, None) infoForm += \ '<center><h1><a href="/users/' + nickname + '/moderation">' + \ translate['Moderation Information'] + \ '</a></h1></center><br>' infoShown = False accounts = [] for subdir, dirs, files in os.walk(base_dir + '/accounts'): for acct in dirs: if not isAccountDir(acct): continue accounts.append(acct) break 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' col = 0 for acct in accounts: acctNickname = acct.split('@')[0] accountDir = os.path.join(base_dir + '/accounts', acct) actor_json = loadJson(accountDir + '.json') if not actor_json: continue actor = actor_json['id'] avatarUrl = '' ext = '' if actor_json.get('icon'): if actor_json['icon'].get('url'): avatarUrl = actor_json['icon']['url'] if '.' in avatarUrl: ext = '.' + avatarUrl.split('.')[-1] acctUrl = \ '/users/' + nickname + '?options=' + actor + ';1;' + \ '/members/' + acctNickname + ext infoForm += '<td>\n<a href="' + acctUrl + '">' infoForm += '<img loading="lazy" style="width:90%" ' infoForm += 'src="' + avatarUrl + '" />' infoForm += '<br><center>' if isModerator(base_dir, acctNickname): infoForm += '<b><u>' + acctNickname + '</u></b>' else: infoForm += acctNickname if isEditor(base_dir, acctNickname): infoForm += ' ✍' 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' suspendedFilename = base_dir + '/accounts/suspended.txt' if os.path.isfile(suspendedFilename): with open(suspendedFilename, 'r') as f: suspendedStr = f.read() infoForm += '<div class="container">\n' infoForm += ' <br><b>' + \ translate['Suspended accounts'] + '</b>' infoForm += ' <br>' + \ translate['These are currently suspended'] infoForm += \ ' <textarea id="message" ' + \ 'name="suspended" style="height:200px" spellcheck="false">' + \ suspendedStr + '</textarea>\n' infoForm += '</div>\n' infoShown = True blockingFilename = base_dir + '/accounts/blocking.txt' if os.path.isfile(blockingFilename): with open(blockingFilename, 'r') as f: blockedStr = f.read() infoForm += '<div class="container">\n' infoForm += \ ' <br><b>' + \ translate['Blocked accounts and hashtags'] + '</b>' infoForm += \ ' <br>' + \ translate[msgStr1] infoForm += \ ' <textarea id="message" ' + \ 'name="blocked" style="height:700px" spellcheck="false">' + \ blockedStr + '</textarea>\n' infoForm += '</div>\n' infoShown = True filtersFilename = base_dir + '/accounts/filters.txt' if os.path.isfile(filtersFilename): 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" ' + \ 'name="filtered" style="height:700px" spellcheck="true">' + \ filteredStr + '</textarea>\n' infoForm += '</div>\n' infoShown = True if not infoShown: infoForm += \ '<center><p>' + \ translate[msgStr2] + \ '</p></center>\n' infoForm += htmlFooter() return infoForm