Youtube replacement domain is configurable

main
Bob Mottram 2020-08-02 10:51:20 +01:00
parent 8c54fffe69
commit 83cac23229
9 changed files with 143 additions and 68 deletions

View File

@ -928,7 +928,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.personCache, self.server.personCache,
self.server.allowDeletion, self.server.allowDeletion,
self.server.proxyType, version, self.server.proxyType, version,
self.server.debug) self.server.debug,
self.server.YTReplacementDomain)
def _postToOutboxThread(self, messageJson: {}) -> bool: def _postToOutboxThread(self, messageJson: {}) -> bool:
"""Creates a thread to send a post """Creates a thread to send a post
@ -2266,7 +2267,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion) self.server.projectVersion,
self.server.YTReplacementDomain)
if hashtagStr: if hashtagStr:
msg = hashtagStr.encode('utf-8') msg = hashtagStr.encode('utf-8')
self._set_headers('text/html', len(msg), self._set_headers('text/html', len(msg),
@ -3771,6 +3773,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.recentPostsCache self.server.recentPostsCache
cachedWebfingers = \ cachedWebfingers = \
self.server.cachedWebfingers self.server.cachedWebfingers
YTReplacementDomain = \
self.server.YTReplacementDomain
msg = \ msg = \
htmlProfile(defaultTimeline, htmlProfile(defaultTimeline,
recentPostsCache, recentPostsCache,
@ -3785,6 +3789,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
cachedWebfingers, cachedWebfingers,
self.server.personCache, self.server.personCache,
YTReplacementDomain,
actorJson['roles'], actorJson['roles'],
None, None) None, None)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
@ -3831,6 +3836,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.recentPostsCache self.server.recentPostsCache
cachedWebfingers = \ cachedWebfingers = \
self.server.cachedWebfingers self.server.cachedWebfingers
YTReplacementDomain = \
self.server.YTReplacementDomain
msg = \ msg = \
htmlProfile(defaultTimeline, htmlProfile(defaultTimeline,
recentPostsCache, recentPostsCache,
@ -3845,6 +3852,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
cachedWebfingers, cachedWebfingers,
self.server.personCache, self.server.personCache,
YTReplacementDomain,
actorJson['skills'], actorJson['skills'],
None, None) None, None)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
@ -4035,7 +4043,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4126,7 +4135,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4216,7 +4226,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4307,7 +4318,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4396,7 +4408,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4461,7 +4474,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.port, self.server.port,
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion) self.server.projectVersion,
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4538,7 +4552,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4624,7 +4639,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.allowDeletion, self.server.allowDeletion,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion, self.server.projectVersion,
self._isMinimal(nickname)) self._isMinimal(nickname),
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4701,7 +4717,8 @@ class PubServer(BaseHTTPRequestHandler):
moderationFeed, moderationFeed,
True, True,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion) self.server.projectVersion,
self.server.YTReplacementDomain)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -4789,6 +4806,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.YTReplacementDomain,
shares, shares,
pageNumber, sharesPerPage) pageNumber, sharesPerPage)
msg = msg.encode('utf-8') msg = msg.encode('utf-8')
@ -4869,6 +4887,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.YTReplacementDomain,
following, following,
pageNumber, pageNumber,
followsPerPage).encode('utf-8') followsPerPage).encode('utf-8')
@ -4947,6 +4966,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.YTReplacementDomain,
followers, followers,
pageNumber, pageNumber,
followsPerPage).encode('utf-8') followsPerPage).encode('utf-8')
@ -5001,6 +5021,7 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.YTReplacementDomain,
None, None).encode('utf-8') None, None).encode('utf-8')
self._set_headers('text/html', self._set_headers('text/html',
len(msg), len(msg),
@ -5379,7 +5400,8 @@ class PubServer(BaseHTTPRequestHandler):
imgDescription, imgDescription,
self.server.useBlurHash) self.server.useBlurHash)
replaceYouTube(postJsonObject) replaceYouTube(postJsonObject,
self.server.YTReplacementDomain)
saveJson(postJsonObject, postFilename) saveJson(postJsonObject, postFilename)
print('Edited blog post, resaved ' + postFilename) print('Edited blog post, resaved ' + postFilename)
return 1 return 1
@ -6933,7 +6955,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.httpPrefix, self.server.httpPrefix,
self.server.projectVersion) self.server.projectVersion,
self.server.YTReplacementDomain)
if hashtagStr: if hashtagStr:
msg = hashtagStr.encode('utf-8') msg = hashtagStr.encode('utf-8')
self._login_headers('text/html', self._login_headers('text/html',
@ -6977,7 +7000,8 @@ class PubServer(BaseHTTPRequestHandler):
self.server.session, self.server.session,
self.server.cachedWebfingers, self.server.cachedWebfingers,
self.server.personCache, self.server.personCache,
self.server.port) self.server.port,
self.server.YTReplacementDomain)
if historyStr: if historyStr:
msg = historyStr.encode('utf-8') msg = historyStr.encode('utf-8')
self._login_headers('text/html', self._login_headers('text/html',
@ -8312,6 +8336,7 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
instanceId: str, clientToServer: bool, instanceId: str, clientToServer: bool,
baseDir: str, domain: str, baseDir: str, domain: str,
onionDomain: str, i2pDomain: str, onionDomain: str, i2pDomain: str,
YTReplacementDomain: str,
port=80, proxyPort=80, httpPrefix='https', port=80, proxyPort=80, httpPrefix='https',
fedList=[], maxMentions=10, maxEmoji=10, fedList=[], maxMentions=10, maxEmoji=10,
authenticatedFetch=False, authenticatedFetch=False,
@ -8348,6 +8373,8 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
print('ERROR: HTTP server failed to start. ' + str(e)) print('ERROR: HTTP server failed to start. ' + str(e))
return False return False
httpd.YTReplacementDomain = YTReplacementDomain
# This counter is used to update the list of blocked domains in memory. # This counter is used to update the list of blocked domains in memory.
# It helps to avoid touching the disk and so improves flooding resistance # It helps to avoid touching the disk and so improves flooding resistance
httpd.blocklistUpdateCtr = 0 httpd.blocklistUpdateCtr = 0
@ -8535,8 +8562,9 @@ def runDaemon(blogsInstance: bool, mediaInstance: bool,
httpd.ocapAlways, maxReplies, httpd.ocapAlways, maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay, domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, debug, maxMentions, maxEmoji, allowDeletion, debug, maxMentions, maxEmoji,
httpd.translate, httpd.translate, unitTest,
unitTest, httpd.acceptedCaps), daemon=True) httpd.YTReplacementDomain,
httpd.acceptedCaps), daemon=True)
print('Creating scheduled post thread') print('Creating scheduled post thread')
httpd.thrPostSchedule = \ httpd.thrPostSchedule = \
threadWithTrace(target=runPostSchedule, threadWithTrace(target=runPostSchedule,

View File

@ -115,6 +115,9 @@ parser.add_argument('--proxy', dest='proxyPort', type=int, default=None,
parser.add_argument('--path', dest='baseDir', parser.add_argument('--path', dest='baseDir',
type=str, default=os.getcwd(), type=str, default=os.getcwd(),
help='Directory in which to store posts') help='Directory in which to store posts')
parser.add_argument('--ytdomain', dest='YTReplacementDomain',
type=str, default=None,
help='Domain used to replace youtube.com')
parser.add_argument('--language', dest='language', parser.add_argument('--language', dest='language',
type=str, default=None, type=str, default=None,
help='Language code, eg. en/fr/de/es') help='Language code, eg. en/fr/de/es')
@ -1791,6 +1794,10 @@ registration = getConfigParam(baseDir, 'registration')
if not registration: if not registration:
registration = False registration = False
YTDomain = getConfigParam(baseDir, 'youtubedomain')
if YTDomain:
args.YTReplacementDomain = YTDomain
if setTheme(baseDir, themeName): if setTheme(baseDir, themeName):
print('Theme set to ' + themeName) print('Theme set to ' + themeName)
@ -1800,6 +1807,7 @@ runDaemon(args.blogsinstance, args.mediainstance,
registration, args.language, __version__, registration, args.language, __version__,
instanceId, args.client, baseDir, instanceId, args.client, baseDir,
domain, onionDomain, i2pDomain, domain, onionDomain, i2pDomain,
args.YTReplacementDomain,
port, proxyPort, httpPrefix, port, proxyPort, httpPrefix,
federationList, args.maxMentions, federationList, args.maxMentions,
args.maxEmoji, args.authenticatedFetch, args.maxEmoji, args.authenticatedFetch,

View File

@ -1328,7 +1328,8 @@ def receiveAnnounce(recentPostsCache: {},
httpPrefix: str, domain: str, onionDomain: str, port: int, httpPrefix: str, domain: str, onionDomain: str, port: int,
sendThreads: [], postLog: [], cachedWebfingers: {}, sendThreads: [], postLog: [], cachedWebfingers: {},
personCache: {}, messageJson: {}, federationList: [], personCache: {}, messageJson: {}, federationList: [],
debug: bool, translate: {}) -> bool: debug: bool, translate: {},
YTReplacementDomain: str) -> bool:
"""Receives an announce activity within the POST section of HTTPServer """Receives an announce activity within the POST section of HTTPServer
""" """
if messageJson['type'] != 'Announce': if messageJson['type'] != 'Announce':
@ -1410,7 +1411,8 @@ def receiveAnnounce(recentPostsCache: {},
' -> ' + messageJson['object']) ' -> ' + messageJson['object'])
postJsonObject = downloadAnnounce(session, baseDir, httpPrefix, postJsonObject = downloadAnnounce(session, baseDir, httpPrefix,
nickname, domain, messageJson, nickname, domain, messageJson,
__version__, translate) __version__, translate,
YTReplacementDomain)
if postJsonObject: if postJsonObject:
if debug: if debug:
print('DEBUG: Announce post downloaded for ' + print('DEBUG: Announce post downloaded for ' +
@ -2055,7 +2057,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
queueFilename: str, destinationFilename: str, queueFilename: str, destinationFilename: str,
maxReplies: int, allowDeletion: bool, maxReplies: int, allowDeletion: bool,
maxMentions: int, maxEmoji: int, translate: {}, maxMentions: int, maxEmoji: int, translate: {},
unitTest: bool) -> bool: unitTest: bool, YTReplacementDomain: str) -> bool:
""" Anything which needs to be done after capabilities checks have passed """ Anything which needs to be done after capabilities checks have passed
""" """
actor = keyId actor = keyId
@ -2132,7 +2134,8 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
personCache, personCache,
messageJson, messageJson,
federationList, federationList,
debug, translate): debug, translate,
YTReplacementDomain):
if debug: if debug:
print('DEBUG: Announce accepted from ' + actor) print('DEBUG: Announce accepted from ' + actor)
@ -2207,7 +2210,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
return False return False
# replace YouTube links, so they get less tracking data # replace YouTube links, so they get less tracking data
replaceYouTube(postJsonObject) replaceYouTube(postJsonObject, YTReplacementDomain)
# list of indexes to be updated # list of indexes to be updated
updateIndexList = ['inbox'] updateIndexList = ['inbox']
@ -2289,7 +2292,7 @@ def inboxAfterCapabilities(recentPostsCache: {}, maxRecentPosts: int,
if isImageMedia(session, baseDir, httpPrefix, if isImageMedia(session, baseDir, httpPrefix,
nickname, domain, postJsonObject, nickname, domain, postJsonObject,
translate): translate, YTReplacementDomain):
# media index will be updated # media index will be updated
updateIndexList.append('tlmedia') updateIndexList.append('tlmedia')
if isBlogPost(postJsonObject): if isBlogPost(postJsonObject):
@ -2411,9 +2414,10 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
domainMaxPostsPerDay: int, accountMaxPostsPerDay: int, domainMaxPostsPerDay: int, accountMaxPostsPerDay: int,
allowDeletion: bool, debug: bool, maxMentions: int, allowDeletion: bool, debug: bool, maxMentions: int,
maxEmoji: int, translate: {}, unitTest: bool, maxEmoji: int, translate: {}, unitTest: bool,
YTReplacementDomain: str,
acceptedCaps=["inbox:write", "objects:read"]) -> None: acceptedCaps=["inbox:write", "objects:read"]) -> None:
"""Processes received items and moves them to """Processes received items and moves them to the appropriate
the appropriate directories directories
""" """
currSessionTime = int(time.time()) currSessionTime = int(time.time())
sessionLastUpdate = currSessionTime sessionLastUpdate = currSessionTime
@ -2829,7 +2833,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
queueFilename, destination, queueFilename, destination,
maxReplies, allowDeletion, maxReplies, allowDeletion,
maxMentions, maxEmoji, maxMentions, maxEmoji,
translate, unitTest) translate, unitTest,
YTReplacementDomain)
else: else:
print('Queue: object capabilities check has failed') print('Queue: object capabilities check has failed')
if debug: if debug:
@ -2852,7 +2857,8 @@ def runInboxQueue(recentPostsCache: {}, maxRecentPosts: int,
queueFilename, destination, queueFilename, destination,
maxReplies, allowDeletion, maxReplies, allowDeletion,
maxMentions, maxEmoji, maxMentions, maxEmoji,
translate, unitTest) translate, unitTest,
YTReplacementDomain)
if debug: if debug:
pprint(queueJson['post']) pprint(queueJson['post'])
print('No capability list within post') print('No capability list within post')

View File

@ -18,10 +18,12 @@ from shutil import rmtree
from shutil import move from shutil import move
def replaceYouTube(postJsonObject: {}) -> None: def replaceYouTube(postJsonObject: {}, replacementDomain: str) -> None:
"""Replace YouTube with invidio.us """Replace YouTube with a replacement domain
This denies Google some, but not all, tracking data This denies Google some, but not all, tracking data
""" """
if not replacementDomain:
return
if not isinstance(postJsonObject['object'], dict): if not isinstance(postJsonObject['object'], dict):
return return
if not postJsonObject['object'].get('content'): if not postJsonObject['object'].get('content'):
@ -30,7 +32,7 @@ def replaceYouTube(postJsonObject: {}) -> None:
return return
postJsonObject['object']['content'] = \ postJsonObject['object']['content'] = \
postJsonObject['object']['content'].replace('www.youtube.com', postJsonObject['object']['content'].replace('www.youtube.com',
'invidio.us') replacementDomain)
def removeMetaData(imageFilename: str, outputFilename: str) -> None: def removeMetaData(imageFilename: str, outputFilename: str) -> None:

View File

@ -44,7 +44,8 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
federationList: [], sendThreads: [], federationList: [], sendThreads: [],
postLog: [], cachedWebfingers: {}, postLog: [], cachedWebfingers: {},
personCache: {}, allowDeletion: bool, personCache: {}, allowDeletion: bool,
proxyType: str, version: str, debug: bool) -> bool: proxyType: str, version: str, debug: bool,
YTReplacementDomain: str) -> bool:
"""post is received by the outbox """post is received by the outbox
Client to server message post Client to server message post
https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery https://www.w3.org/TR/activitypub/#client-to-server-outbox-delivery
@ -104,7 +105,7 @@ def postMessageToOutbox(messageJson: {}, postToNickname: str,
print('DEBUG: domain is blocked: ' + messageJson['actor']) print('DEBUG: domain is blocked: ' + messageJson['actor'])
return False return False
# replace youtube, so that google gets less tracking data # replace youtube, so that google gets less tracking data
replaceYouTube(messageJson) replaceYouTube(messageJson, YTReplacementDomain)
# https://www.w3.org/TR/activitypub/#create-activity-outbox # https://www.w3.org/TR/activitypub/#create-activity-outbox
messageJson['object']['attributedTo'] = messageJson['actor'] messageJson['object']['attributedTo'] = messageJson['actor']
if messageJson['object'].get('attachment'): if messageJson['object'].get('attachment'):

View File

@ -2418,14 +2418,16 @@ def isDM(postJsonObject: {}) -> bool:
def isImageMedia(session, baseDir: str, httpPrefix: str, def isImageMedia(session, baseDir: str, httpPrefix: str,
nickname: str, domain: str, nickname: str, domain: str,
postJsonObject: {}, translate: {}) -> bool: postJsonObject: {}, translate: {},
YTReplacementDomain: str) -> bool:
"""Returns true if the given post has attached image media """Returns true if the given post has attached image media
""" """
if postJsonObject['type'] == 'Announce': if postJsonObject['type'] == 'Announce':
postJsonAnnounce = \ postJsonAnnounce = \
downloadAnnounce(session, baseDir, httpPrefix, downloadAnnounce(session, baseDir, httpPrefix,
nickname, domain, postJsonObject, nickname, domain, postJsonObject,
__version__, translate) __version__, translate,
YTReplacementDomain)
if postJsonAnnounce: if postJsonAnnounce:
postJsonObject = postJsonAnnounce postJsonObject = postJsonAnnounce
if postJsonObject['type'] != 'Create': if postJsonObject['type'] != 'Create':
@ -3153,7 +3155,7 @@ def rejectAnnounce(announceFilename: str):
def downloadAnnounce(session, baseDir: str, httpPrefix: str, def downloadAnnounce(session, baseDir: str, httpPrefix: str,
nickname: str, domain: str, nickname: str, domain: str,
postJsonObject: {}, projectVersion: str, postJsonObject: {}, projectVersion: str,
translate: {}) -> {}: translate: {}, YTReplacementDomain: str) -> {}:
"""Download the post referenced by an announce """Download the post referenced by an announce
""" """
if not postJsonObject.get('object'): if not postJsonObject.get('object'):
@ -3289,7 +3291,7 @@ def downloadAnnounce(session, baseDir: str, httpPrefix: str,
rejectAnnounce(announceFilename) rejectAnnounce(announceFilename)
return None return None
postJsonObject = announcedJson postJsonObject = announcedJson
replaceYouTube(postJsonObject) replaceYouTube(postJsonObject, YTReplacementDomain)
if saveJson(postJsonObject, announceFilename): if saveJson(postJsonObject, announceFilename):
return postJsonObject return postJsonObject
return None return None

View File

@ -103,7 +103,8 @@ def updatePostSchedule(baseDir: str, handle: str, httpd,
httpd.allowDeletion, httpd.allowDeletion,
httpd.proxyType, httpd.proxyType,
httpd.projectVersion, httpd.projectVersion,
httpd.debug): httpd.debug,
httpd.YTReplacementDomain):
indexLines.remove(line) indexLines.remove(line)
os.remove(postFilename) os.remove(postFilename)
continue continue

View File

@ -286,7 +286,7 @@ def createServerAlice(path: str, domain: str, port: int,
print('Server running: Alice') print('Server running: Alice')
runDaemon(False, False, 5, True, True, 'en', __version__, runDaemon(False, False, 5, True, True, 'en', __version__,
"instanceId", False, path, domain, "instanceId", False, path, domain,
onionDomain, i2pDomain, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
noreply, nolike, nopics, noannounce, cw, ocapAlways, noreply, nolike, nopics, noannounce, cw, ocapAlways,
proxyType, maxReplies, proxyType, maxReplies,
@ -351,7 +351,7 @@ def createServerBob(path: str, domain: str, port: int,
print('Server running: Bob') print('Server running: Bob')
runDaemon(False, False, 5, True, True, 'en', __version__, runDaemon(False, False, 5, True, True, 'en', __version__,
"instanceId", False, path, domain, "instanceId", False, path, domain,
onionDomain, i2pDomain, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
noreply, nolike, nopics, noannounce, cw, ocapAlways, noreply, nolike, nopics, noannounce, cw, ocapAlways,
proxyType, maxReplies, proxyType, maxReplies,
@ -393,7 +393,7 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
print('Server running: Eve') print('Server running: Eve')
runDaemon(False, False, 5, True, True, 'en', __version__, runDaemon(False, False, 5, True, True, 'en', __version__,
"instanceId", False, path, domain, "instanceId", False, path, domain,
onionDomain, i2pDomain, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
noreply, nolike, nopics, noannounce, cw, ocapAlways, noreply, nolike, nopics, noannounce, cw, ocapAlways,
proxyType, maxReplies, allowDeletion, True, True, False, proxyType, maxReplies, allowDeletion, True, True, False,

View File

@ -691,7 +691,8 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
baseDir: str, hashtag: str, pageNumber: int, baseDir: str, hashtag: str, pageNumber: int,
postsPerPage: int, postsPerPage: int,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
httpPrefix: str, projectVersion: str) -> str: httpPrefix: str, projectVersion: str,
YTReplacementDomain: str) -> str:
"""Show a page containing search results for a hashtag """Show a page containing search results for a hashtag
""" """
if hashtag.startswith('#'): if hashtag.startswith('#'):
@ -795,6 +796,7 @@ def htmlHashtagSearch(nickname: str, domain: str, port: int,
None, True, allowDeletion, None, True, allowDeletion,
httpPrefix, projectVersion, httpPrefix, projectVersion,
'search', 'search',
YTReplacementDomain,
showIndividualPostIcons, showIndividualPostIcons,
showIndividualPostIcons, showIndividualPostIcons,
False, False, False) False, False, False)
@ -951,7 +953,8 @@ def htmlHistorySearch(translate: {}, baseDir: str,
session, session,
wfRequest, wfRequest,
personCache: {}, personCache: {},
port: int) -> str: port: int,
YTReplacementDomain: str) -> str:
"""Show a page containing search results for your post history """Show a page containing search results for your post history
""" """
if historysearch.startswith('!'): if historysearch.startswith('!'):
@ -1022,6 +1025,7 @@ def htmlHistorySearch(translate: {}, baseDir: str,
None, True, allowDeletion, None, True, allowDeletion,
httpPrefix, projectVersion, httpPrefix, projectVersion,
'search', 'search',
YTReplacementDomain,
showIndividualPostIcons, showIndividualPostIcons,
showIndividualPostIcons, showIndividualPostIcons,
False, False, False) False, False, False)
@ -2290,7 +2294,8 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
authorized: bool, ocapAlways: bool, authorized: bool, ocapAlways: bool,
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
projectVersion: str) -> str: projectVersion: str,
YTReplacementDomain: str) -> str:
"""Shows posts on the profile screen """Shows posts on the profile screen
These should only be public posts These should only be public posts
""" """
@ -2323,6 +2328,7 @@ def htmlProfilePosts(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, port, item, nickname, domain, port, item,
None, True, False, None, True, False,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
False, False, False, True, False) False, False, False, True, False)
if postStr: if postStr:
profileStr += postStr profileStr += postStr
@ -2566,6 +2572,7 @@ def htmlProfile(defaultTimeline: str,
baseDir: str, httpPrefix: str, authorized: bool, baseDir: str, httpPrefix: str, authorized: bool,
ocapAlways: bool, profileJson: {}, selected: str, ocapAlways: bool, profileJson: {}, selected: str,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
YTReplacementDomain: str,
extraJson=None, extraJson=None,
pageNumber=None, maxItemsPerPage=None) -> str: pageNumber=None, maxItemsPerPage=None) -> str:
"""Show the profile page as html """Show the profile page as html
@ -2823,7 +2830,8 @@ def htmlProfile(defaultTimeline: str,
baseDir, httpPrefix, authorized, baseDir, httpPrefix, authorized,
ocapAlways, nickname, domain, port, ocapAlways, nickname, domain, port,
session, wfRequest, personCache, session, wfRequest, personCache,
projectVersion) + licenseStr projectVersion,
YTReplacementDomain) + licenseStr
if selected == 'following': if selected == 'following':
profileStr += \ profileStr += \
htmlProfileFollowing(translate, baseDir, httpPrefix, htmlProfileFollowing(translate, baseDir, httpPrefix,
@ -3602,7 +3610,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
avatarUrl: str, showAvatarOptions: bool, avatarUrl: str, showAvatarOptions: bool,
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
boxName: str, showRepeats=True, boxName: str, YTReplacementDomain: str,
showRepeats=True,
showIcons=False, showIcons=False,
manuallyApprovesFollowers=False, manuallyApprovesFollowers=False,
showPublicOnly=False, showPublicOnly=False,
@ -3729,7 +3738,8 @@ def individualPostAsHtml(recentPostsCache: {}, maxRecentPosts: int,
postJsonAnnounce = \ postJsonAnnounce = \
downloadAnnounce(session, baseDir, httpPrefix, downloadAnnounce(session, baseDir, httpPrefix,
nickname, domain, postJsonObject, nickname, domain, postJsonObject,
projectVersion, translate) projectVersion, translate,
YTReplacementDomain)
if not postJsonAnnounce: if not postJsonAnnounce:
return '' return ''
postJsonObject = postJsonAnnounce postJsonObject = postJsonAnnounce
@ -4350,7 +4360,8 @@ def htmlTimeline(defaultTimeline: str,
boxName: str, allowDeletion: bool, boxName: str, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
manuallyApproveFollowers: bool, manuallyApproveFollowers: bool,
minimal: bool) -> str: minimal: bool,
YTReplacementDomain: str) -> str:
"""Show the timeline as html """Show the timeline as html
""" """
accountDir = baseDir + '/accounts/' + nickname + '@' + domain accountDir = baseDir + '/accounts/' + nickname + '@' + domain
@ -4793,6 +4804,7 @@ def htmlTimeline(defaultTimeline: str,
allowDeletion, allowDeletion,
httpPrefix, projectVersion, httpPrefix, projectVersion,
boxName, boxName,
YTReplacementDomain,
boxName != 'dm', boxName != 'dm',
showIndividualPostIcons, showIndividualPostIcons,
manuallyApproveFollowers, manuallyApproveFollowers,
@ -4823,7 +4835,8 @@ def htmlShares(defaultTimeline: str,
session, baseDir: str, wfRequest: {}, personCache: {}, session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str) -> str: httpPrefix: str, projectVersion: str,
YTReplacementDomain: str) -> str:
"""Show the shares timeline as html """Show the shares timeline as html
""" """
manuallyApproveFollowers = \ manuallyApproveFollowers = \
@ -4835,7 +4848,7 @@ def htmlShares(defaultTimeline: str,
nickname, domain, port, None, nickname, domain, port, None,
'tlshares', allowDeletion, 'tlshares', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers, httpPrefix, projectVersion, manuallyApproveFollowers,
False) False, YTReplacementDomain)
def htmlInbox(defaultTimeline: str, def htmlInbox(defaultTimeline: str,
@ -4845,7 +4858,7 @@ def htmlInbox(defaultTimeline: str,
nickname: str, domain: str, port: int, inboxJson: {}, nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the inbox as html """Show the inbox as html
""" """
manuallyApproveFollowers = \ manuallyApproveFollowers = \
@ -4857,7 +4870,7 @@ def htmlInbox(defaultTimeline: str,
nickname, domain, port, inboxJson, nickname, domain, port, inboxJson,
'inbox', allowDeletion, 'inbox', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers, httpPrefix, projectVersion, manuallyApproveFollowers,
minimal) minimal, YTReplacementDomain)
def htmlBookmarks(defaultTimeline: str, def htmlBookmarks(defaultTimeline: str,
@ -4867,7 +4880,7 @@ def htmlBookmarks(defaultTimeline: str,
nickname: str, domain: str, port: int, bookmarksJson: {}, nickname: str, domain: str, port: int, bookmarksJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the bookmarks as html """Show the bookmarks as html
""" """
manuallyApproveFollowers = \ manuallyApproveFollowers = \
@ -4879,7 +4892,7 @@ def htmlBookmarks(defaultTimeline: str,
nickname, domain, port, bookmarksJson, nickname, domain, port, bookmarksJson,
'tlbookmarks', allowDeletion, 'tlbookmarks', allowDeletion,
httpPrefix, projectVersion, manuallyApproveFollowers, httpPrefix, projectVersion, manuallyApproveFollowers,
minimal) minimal, YTReplacementDomain)
def htmlInboxDMs(defaultTimeline: str, def htmlInboxDMs(defaultTimeline: str,
@ -4889,14 +4902,15 @@ def htmlInboxDMs(defaultTimeline: str,
nickname: str, domain: str, port: int, inboxJson: {}, nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the DM timeline as html """Show the DM timeline as html
""" """
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber, translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache, itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'dm', allowDeletion, nickname, domain, port, inboxJson, 'dm', allowDeletion,
httpPrefix, projectVersion, False, minimal) httpPrefix, projectVersion, False, minimal,
YTReplacementDomain)
def htmlInboxReplies(defaultTimeline: str, def htmlInboxReplies(defaultTimeline: str,
@ -4906,7 +4920,7 @@ def htmlInboxReplies(defaultTimeline: str,
nickname: str, domain: str, port: int, inboxJson: {}, nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the replies timeline as html """Show the replies timeline as html
""" """
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
@ -4914,7 +4928,7 @@ def htmlInboxReplies(defaultTimeline: str,
itemsPerPage, session, baseDir, wfRequest, personCache, itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlreplies', nickname, domain, port, inboxJson, 'tlreplies',
allowDeletion, httpPrefix, projectVersion, False, allowDeletion, httpPrefix, projectVersion, False,
minimal) minimal, YTReplacementDomain)
def htmlInboxMedia(defaultTimeline: str, def htmlInboxMedia(defaultTimeline: str,
@ -4924,7 +4938,7 @@ def htmlInboxMedia(defaultTimeline: str,
nickname: str, domain: str, port: int, inboxJson: {}, nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the media timeline as html """Show the media timeline as html
""" """
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
@ -4932,7 +4946,7 @@ def htmlInboxMedia(defaultTimeline: str,
itemsPerPage, session, baseDir, wfRequest, personCache, itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlmedia', nickname, domain, port, inboxJson, 'tlmedia',
allowDeletion, httpPrefix, projectVersion, False, allowDeletion, httpPrefix, projectVersion, False,
minimal) minimal, YTReplacementDomain)
def htmlInboxBlogs(defaultTimeline: str, def htmlInboxBlogs(defaultTimeline: str,
@ -4942,7 +4956,7 @@ def htmlInboxBlogs(defaultTimeline: str,
nickname: str, domain: str, port: int, inboxJson: {}, nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the blogs timeline as html """Show the blogs timeline as html
""" """
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
@ -4950,7 +4964,7 @@ def htmlInboxBlogs(defaultTimeline: str,
itemsPerPage, session, baseDir, wfRequest, personCache, itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'tlblogs', nickname, domain, port, inboxJson, 'tlblogs',
allowDeletion, httpPrefix, projectVersion, False, allowDeletion, httpPrefix, projectVersion, False,
minimal) minimal, YTReplacementDomain)
def htmlModeration(defaultTimeline: str, def htmlModeration(defaultTimeline: str,
@ -4959,14 +4973,16 @@ def htmlModeration(defaultTimeline: str,
session, baseDir: str, wfRequest: {}, personCache: {}, session, baseDir: str, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, inboxJson: {}, nickname: str, domain: str, port: int, inboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str) -> str: httpPrefix: str, projectVersion: str,
YTReplacementDomain: str) -> str:
"""Show the moderation feed as html """Show the moderation feed as html
""" """
return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts, return htmlTimeline(defaultTimeline, recentPostsCache, maxRecentPosts,
translate, pageNumber, translate, pageNumber,
itemsPerPage, session, baseDir, wfRequest, personCache, itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, inboxJson, 'moderation', nickname, domain, port, inboxJson, 'moderation',
allowDeletion, httpPrefix, projectVersion, True, False) allowDeletion, httpPrefix, projectVersion, True, False,
YTReplacementDomain)
def htmlOutbox(defaultTimeline: str, def htmlOutbox(defaultTimeline: str,
@ -4976,7 +4992,7 @@ def htmlOutbox(defaultTimeline: str,
nickname: str, domain: str, port: int, outboxJson: {}, nickname: str, domain: str, port: int, outboxJson: {},
allowDeletion: bool, allowDeletion: bool,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
minimal: bool) -> str: minimal: bool, YTReplacementDomain: str) -> str:
"""Show the Outbox as html """Show the Outbox as html
""" """
manuallyApproveFollowers = \ manuallyApproveFollowers = \
@ -4986,7 +5002,8 @@ def htmlOutbox(defaultTimeline: str,
itemsPerPage, session, baseDir, wfRequest, personCache, itemsPerPage, session, baseDir, wfRequest, personCache,
nickname, domain, port, outboxJson, 'outbox', nickname, domain, port, outboxJson, 'outbox',
allowDeletion, httpPrefix, projectVersion, allowDeletion, httpPrefix, projectVersion,
manuallyApproveFollowers, minimal) manuallyApproveFollowers, minimal,
YTReplacementDomain)
def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int, def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
@ -4994,7 +5011,8 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
baseDir: str, session, wfRequest: {}, personCache: {}, baseDir: str, session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, authorized: bool, nickname: str, domain: str, port: int, authorized: bool,
postJsonObject: {}, httpPrefix: str, postJsonObject: {}, httpPrefix: str,
projectVersion: str, likedBy: str) -> str: projectVersion: str, likedBy: str,
YTReplacementDomain: str) -> str:
"""Show an individual post as html """Show an individual post as html
""" """
iconsDir = getIconsDir(baseDir) iconsDir = getIconsDir(baseDir)
@ -5038,6 +5056,7 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, port, postJsonObject, nickname, domain, port, postJsonObject,
None, True, False, None, True, False,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
False, authorized, False, False, False) False, authorized, False, False, False)
messageId = postJsonObject['id'].replace('/activity', '') messageId = postJsonObject['id'].replace('/activity', '')
@ -5060,6 +5079,7 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
postJsonObject, postJsonObject,
None, True, False, None, True, False,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
False, authorized, False, authorized,
False, False, False) + postStr False, False, False) + postStr
@ -5085,6 +5105,7 @@ def htmlIndividualPost(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, port, item, nickname, domain, port, item,
None, True, False, None, True, False,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
False, authorized, False, authorized,
False, False, False) False, False, False)
cssFilename = baseDir + '/epicyon-profile.css' cssFilename = baseDir + '/epicyon-profile.css'
@ -5102,7 +5123,8 @@ def htmlPostReplies(recentPostsCache: {}, maxRecentPosts: int,
translate: {}, baseDir: str, translate: {}, baseDir: str,
session, wfRequest: {}, personCache: {}, session, wfRequest: {}, personCache: {},
nickname: str, domain: str, port: int, repliesJson: {}, nickname: str, domain: str, port: int, repliesJson: {},
httpPrefix: str, projectVersion: str) -> str: httpPrefix: str, projectVersion: str,
YTReplacementDomain: str) -> str:
"""Show the replies to an individual post as html """Show the replies to an individual post as html
""" """
iconsDir = getIconsDir(baseDir) iconsDir = getIconsDir(baseDir)
@ -5116,6 +5138,7 @@ def htmlPostReplies(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, port, item, nickname, domain, port, item,
None, True, False, None, True, False,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
False, False, False, False, False) False, False, False, False, False)
cssFilename = baseDir + '/epicyon-profile.css' cssFilename = baseDir + '/epicyon-profile.css'
@ -5203,7 +5226,8 @@ def htmlDeletePost(recentPostsCache: {}, maxRecentPosts: int,
session, baseDir: str, messageId: str, session, baseDir: str, messageId: str,
httpPrefix: str, projectVersion: str, httpPrefix: str, projectVersion: str,
wfRequest: {}, personCache: {}, wfRequest: {}, personCache: {},
callingDomain: str) -> str: callingDomain: str,
YTReplacementDomain: str) -> str:
"""Shows a screen asking to confirm the deletion of a post """Shows a screen asking to confirm the deletion of a post
""" """
if '/statuses/' not in messageId: if '/statuses/' not in messageId:
@ -5247,6 +5271,7 @@ def htmlDeletePost(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, port, postJsonObject, nickname, domain, port, postJsonObject,
None, True, False, None, True, False,
httpPrefix, projectVersion, 'outbox', httpPrefix, projectVersion, 'outbox',
YTReplacementDomain,
False, False, False, False, False) False, False, False, False, False)
deletePostStr += '<center>' deletePostStr += '<center>'
deletePostStr += \ deletePostStr += \
@ -6195,7 +6220,8 @@ def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
nickname: str, domain: str, port: int, nickname: str, domain: str, port: int,
profileHandle: str, profileHandle: str,
session, cachedWebfingers: {}, personCache: {}, session, cachedWebfingers: {}, personCache: {},
debug: bool, projectVersion: str) -> str: debug: bool, projectVersion: str,
YTReplacementDomain: str) -> str:
"""Show a profile page after a search for a fediverse address """Show a profile page after a search for a fediverse address
""" """
if '/users/' in profileHandle or \ if '/users/' in profileHandle or \
@ -6400,6 +6426,7 @@ def htmlProfileAfterSearch(recentPostsCache: {}, maxRecentPosts: int,
nickname, domain, port, nickname, domain, port,
item, avatarUrl, False, False, item, avatarUrl, False, False,
httpPrefix, projectVersion, 'inbox', httpPrefix, projectVersion, 'inbox',
YTReplacementDomain,
False, False, False, False, False) False, False, False, False, False)
i += 1 i += 1
if i >= 20: if i >= 20: