Deprecate blurhashes

They were computationally expensive, and similar functionality could be achieved in a simpler way with css
merge-requests/8/head
Bob Mottram 2021-01-07 15:38:55 +00:00
parent 13c05db596
commit cfaf5a5792
10 changed files with 57 additions and 291 deletions

View File

@ -24,8 +24,8 @@ On Arch/Parabola:
``` bash ``` bash
sudo pacman -S tor python-pip python-pysocks python-pycryptodome \ sudo pacman -S tor python-pip python-pysocks python-pycryptodome \
imagemagick python-pillow python-requests \ imagemagick python-requests \
perl-image-exiftool python-numpy python-dateutil \ perl-image-exiftool python-dateutil \
certbot flake8 bandit certbot flake8 bandit
sudo pip3 install pyqrcode pypng sudo pip3 install pyqrcode pypng
``` ```
@ -35,9 +35,9 @@ Or on Debian:
``` bash ``` bash
sudo apt install -y \ sudo apt install -y \
tor python3-socks imagemagick \ tor python3-socks imagemagick \
python3-numpy python3-setuptools \ python3-setuptools \
python3-crypto python3-pycryptodome \ python3-crypto python3-pycryptodome \
python3-dateutil python3-pil.imagetk \ python3-dateutil \
python3-idna python3-requests \ python3-idna python3-requests \
python3-django-timezone-field \ python3-django-timezone-field \
libimage-exiftool-perl python3-flake8 \ libimage-exiftool-perl python3-flake8 \

View File

@ -1,184 +0,0 @@
"""
Copyright (c) 2019 Lorenz Diener
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
* You and any organization you work for may not promote white supremacy, hate
speech and homo- or transphobia - this license is void if you do.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
https://github.com/halcy/blurhash-python
Pure python blurhash decoder with no additional dependencies, for
both de- and encoding.
Very close port of the original Swift implementation by Dag Ågren.
"""
import math
# Alphabet for base 83
alphabet = \
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" + \
"abcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~"
alphabet_values = dict(zip(alphabet, range(len(alphabet))))
def _base83_encode(value, length):
"""
Decodes an integer to a base83 string, as used in blurhash.
Length is how long the resulting string should be. Will complain
if the specified length is too short.
"""
if int(value) // (83 ** (length)) != 0:
raise ValueError("Specified length is too short to " +
"encode given value.")
result = ""
for i in range(1, length + 1):
digit = int(value) // (83 ** (length - i)) % 83
result += alphabet[int(digit)]
return result
def _srgb_to_linear(value):
"""
srgb 0-255 integer to linear 0.0-1.0 floating point conversion.
"""
value = float(value) / 255.0
if value <= 0.04045:
return value / 12.92
return math.pow((value + 0.055) / 1.055, 2.4)
def _sign_pow(value, exp):
"""
Sign-preserving exponentiation.
"""
return math.copysign(math.pow(abs(value), exp), value)
def _linear_to_srgb(value):
"""
linear 0.0-1.0 floating point to srgb 0-255 integer conversion.
"""
value = max(0.0, min(1.0, value))
if value <= 0.0031308:
return int(value * 12.92 * 255 + 0.5)
return int((1.055 * math.pow(value, 1 / 2.4) - 0.055) * 255 + 0.5)
def blurhash_encode(image, components_x=4, components_y=4, linear=False):
"""
Calculates the blurhash for an image using the given x and y
component counts.
Image should be a 3-dimensional array, with the first dimension
being y, the second being x, and the third being the three rgb
components that are assumed to be 0-255 srgb integers
(incidentally, this is the format you will get from a PIL RGB image).
You can also pass in already linear data - to do this, set linear
to True. This is useful if you want to encode a version of your
image resized to a smaller size (which you should ideally do in
linear colour).
"""
if components_x < 1 or components_x > 9 or \
components_y < 1 or components_y > 9:
raise ValueError("x and y component counts must be " +
"between 1 and 9 inclusive.")
height = float(len(image))
width = float(len(image[0]))
# Convert to linear if neeeded
image_linear = []
if linear is False:
for y in range(int(height)):
image_linear_line = []
for x in range(int(width)):
image_linear_line.append([
_srgb_to_linear(image[y][x][0]),
_srgb_to_linear(image[y][x][1]),
_srgb_to_linear(image[y][x][2])
])
image_linear.append(image_linear_line)
else:
image_linear = image
# Calculate components
components = []
max_ac_component = 0.0
for j in range(components_y):
for i in range(components_x):
norm_factor = 1.0 if (i == 0 and j == 0) else 2.0
component = [0.0, 0.0, 0.0]
for y in range(int(height)):
for x in range(int(width)):
basis = \
norm_factor * \
math.cos(math.pi * float(i) * float(x) / width) * \
math.cos(math.pi * float(j) * float(y) / height)
component[0] += basis * image_linear[y][x][0]
component[1] += basis * image_linear[y][x][1]
component[2] += basis * image_linear[y][x][2]
component[0] /= (width * height)
component[1] /= (width * height)
component[2] /= (width * height)
components.append(component)
if not (i == 0 and j == 0):
max_ac_component = \
max(max_ac_component, abs(component[0]),
abs(component[1]), abs(component[2]))
# Encode components
dc_value = (_linear_to_srgb(components[0][0]) << 16) + \
(_linear_to_srgb(components[0][1]) << 8) + \
_linear_to_srgb(components[0][2])
quant_max_ac_component = int(max(0, min(82,
math.floor(max_ac_component *
166 - 0.5))))
ac_component_norm_factor = float(quant_max_ac_component + 1) / 166.0
ac_values = []
for r, g, b in components[1:]:
r2 = r / ac_component_norm_factor
g2 = g / ac_component_norm_factor
b2 = b / ac_component_norm_factor
r3 = math.floor(_sign_pow(r2, 0.5) * 9.0 + 9.5)
g3 = math.floor(_sign_pow(g2, 0.5) * 9.0 + 9.5)
b3 = math.floor(_sign_pow(b2, 0.5) * 9.0 + 9.5)
ac_values.append(
int(max(0.0, min(18.0, r3))) * 19 * 19 +
int(max(0.0, min(18.0, g3))) * 19 +
int(max(0.0, min(18.0, b3)))
)
# Build final blurhash
blurhash = ""
blurhashValue = (components_x - 1) + (components_y - 1) * 9
blurhash += _base83_encode(blurhashValue, 1)
blurhash += _base83_encode(quant_max_ac_component, 1)
blurhash += _base83_encode(dc_value, 4)
for ac_value in ac_values:
blurhash += _base83_encode(ac_value, 2)
return blurhash

View File

@ -11977,7 +11977,6 @@ class PubServer(BaseHTTPRequestHandler):
False, False, False, commentsEnabled, False, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['replyTo'], fields['replyTo'], fields['replyTo'], fields['replyTo'],
fields['subject'], fields['schedulePost'], fields['subject'], fields['schedulePost'],
fields['eventDate'], fields['eventTime'], fields['eventDate'], fields['eventTime'],
@ -12032,7 +12031,6 @@ class PubServer(BaseHTTPRequestHandler):
False, False, False, commentsEnabled, False, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['replyTo'], fields['replyTo'], fields['replyTo'], fields['replyTo'],
fields['subject'], fields['subject'],
fields['schedulePost'], fields['schedulePost'],
@ -12112,8 +12110,7 @@ class PubServer(BaseHTTPRequestHandler):
postJsonObject['object'], postJsonObject['object'],
filename, filename,
attachmentMediaType, attachmentMediaType,
imgDescription, imgDescription)
self.server.useBlurHash)
replaceYouTube(postJsonObject, replaceYouTube(postJsonObject,
self.server.YTReplacementDomain) self.server.YTReplacementDomain)
@ -12144,7 +12141,6 @@ class PubServer(BaseHTTPRequestHandler):
False, False, False, commentsEnabled, False, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['replyTo'], fields['replyTo'],
fields['replyTo'], fields['replyTo'],
fields['subject'], fields['subject'],
@ -12177,7 +12173,6 @@ class PubServer(BaseHTTPRequestHandler):
commentsEnabled, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['replyTo'], fields['replyTo'],
fields['replyTo'], fields['replyTo'],
fields['subject'], fields['subject'],
@ -12230,7 +12225,6 @@ class PubServer(BaseHTTPRequestHandler):
False, False, commentsEnabled, False, False, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['subject'], fields['subject'],
fields['schedulePost'], fields['schedulePost'],
fields['eventDate'], fields['eventDate'],
@ -12268,7 +12262,6 @@ class PubServer(BaseHTTPRequestHandler):
commentsEnabled, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['replyTo'], fields['replyTo'],
fields['replyTo'], fields['replyTo'],
fields['subject'], fields['subject'],
@ -12307,7 +12300,6 @@ class PubServer(BaseHTTPRequestHandler):
True, False, False, False, True, False, False, False,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
None, None, None, None,
fields['subject'], fields['subject'],
True, fields['schedulePost'], True, fields['schedulePost'],
@ -12340,7 +12332,6 @@ class PubServer(BaseHTTPRequestHandler):
True, False, False, True, True, False, False, True,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
self.server.debug, fields['subject']) self.server.debug, fields['subject'])
if messageJson: if messageJson:
if self._postToOutbox(messageJson, __version__, nickname): if self._postToOutbox(messageJson, __version__, nickname):
@ -12371,7 +12362,6 @@ class PubServer(BaseHTTPRequestHandler):
commentsEnabled, commentsEnabled,
filename, attachmentMediaType, filename, attachmentMediaType,
fields['imageDescription'], fields['imageDescription'],
self.server.useBlurHash,
fields['subject'], fields['subject'],
int(fields['duration'])) int(fields['duration']))
if messageJson: if messageJson:
@ -13464,7 +13454,6 @@ def runDaemon(verifyAllSignatures: bool,
domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864, domainMaxPostsPerDay=8640, accountMaxPostsPerDay=864,
allowDeletion=False, debug=False, unitTest=False, allowDeletion=False, debug=False, unitTest=False,
instanceOnlySkillsSearch=False, sendThreads=[], instanceOnlySkillsSearch=False, sendThreads=[],
useBlurHash=False,
manualFollowerApproval=True) -> None: manualFollowerApproval=True) -> None:
if len(domain) == 0: if len(domain) == 0:
domain = 'localhost' domain = 'localhost'
@ -13520,7 +13509,6 @@ def runDaemon(verifyAllSignatures: bool,
httpd.manualFollowerApproval = manualFollowerApproval httpd.manualFollowerApproval = manualFollowerApproval
httpd.onionDomain = onionDomain httpd.onionDomain = onionDomain
httpd.i2pDomain = i2pDomain httpd.i2pDomain = i2pDomain
httpd.useBlurHash = useBlurHash
httpd.mediaInstance = mediaInstance httpd.mediaInstance = mediaInstance
httpd.blogsInstance = blogsInstance httpd.blogsInstance = blogsInstance
httpd.newsInstance = newsInstance httpd.newsInstance = newsInstance

View File

@ -375,9 +375,6 @@ parser.add_argument('--attach', dest='attach', type=str,
default=None, help='File to attach to a post') default=None, help='File to attach to a post')
parser.add_argument('--imagedescription', dest='imageDescription', type=str, parser.add_argument('--imagedescription', dest='imageDescription', type=str,
default=None, help='Description of an attached image') default=None, help='Description of an attached image')
parser.add_argument("--blurhash", type=str2bool, nargs='?',
const=True, default=False,
help="Create blurhash for an image")
parser.add_argument('--warning', '--warn', '--cwsubject', '--subject', parser.add_argument('--warning', '--warn', '--cwsubject', '--subject',
dest='subject', type=str, default=None, dest='subject', type=str, default=None,
help='Subject of content warning') help='Subject of content warning')
@ -1002,7 +999,6 @@ if args.message:
followersOnly = args.followersonly followersOnly = args.followersonly
clientToServer = args.client clientToServer = args.client
attachedImageDescription = args.imageDescription attachedImageDescription = args.imageDescription
useBlurhash = args.blurhash
sendThreads = [] sendThreads = []
postLog = [] postLog = []
personCache = {} personCache = {}
@ -1023,7 +1019,7 @@ if args.message:
toNickname, toDomain, toPort, ccUrl, toNickname, toDomain, toPort, ccUrl,
httpPrefix, sendMessage, followersOnly, httpPrefix, sendMessage, followersOnly,
args.commentsEnabled, attach, mediaType, args.commentsEnabled, attach, mediaType,
attachedImageDescription, useBlurhash, attachedImageDescription,
cachedWebfingers, personCache, isArticle, cachedWebfingers, personCache, isArticle,
args.debug, replyTo, replyTo, subject) args.debug, replyTo, replyTo, subject)
for i in range(10): for i in range(10):
@ -1864,7 +1860,6 @@ if args.unfilterStr:
sys.exit() sys.exit()
if args.testdata: if args.testdata:
useBlurhash = False
nickname = 'testuser567' nickname = 'testuser567'
password = 'boringpassword' password = 'boringpassword'
print('Generating some test data for user: ' + nickname) print('Generating some test data for user: ' + nickname)
@ -1942,8 +1937,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription)
useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"Zoiks!!!", "Zoiks!!!",
testFollowersOnly, testFollowersOnly,
@ -1951,8 +1945,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription)
useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"Hey scoob we need like a hundred more #milkshakes", "Hey scoob we need like a hundred more #milkshakes",
testFollowersOnly, testFollowersOnly,
@ -1960,8 +1953,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription)
useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"Getting kinda spooky around here", "Getting kinda spooky around here",
testFollowersOnly, testFollowersOnly,
@ -1970,7 +1962,7 @@ if args.testdata:
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription,
useBlurhash, 'someone') 'someone')
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"And they would have gotten away with it too" + "And they would have gotten away with it too" +
"if it wasn't for those pesky hackers", "if it wasn't for those pesky hackers",
@ -1979,8 +1971,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
'img/logo.png', 'image/png', 'img/logo.png', 'image/png',
'Description of image', 'Description of image')
useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"man these centralized sites are like the worst!", "man these centralized sites are like the worst!",
testFollowersOnly, testFollowersOnly,
@ -1988,8 +1979,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription)
useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"another mystery solved #test", "another mystery solved #test",
testFollowersOnly, testFollowersOnly,
@ -1997,8 +1987,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription)
useBlurhash)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"let's go bowling", "let's go bowling",
testFollowersOnly, testFollowersOnly,
@ -2006,8 +1995,7 @@ if args.testdata:
testClientToServer, testClientToServer,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testImageDescription, testMediaType, testImageDescription)
useBlurhash)
domainFull = domain + ':' + str(port) domainFull = domain + ':' + str(port)
clearFollows(baseDir, nickname, domain) clearFollows(baseDir, nickname, domain)
@ -2179,4 +2167,4 @@ if __name__ == "__main__":
args.accountMaxPostsPerDay, args.accountMaxPostsPerDay,
args.allowdeletion, debug, False, args.allowdeletion, debug, False,
args.instanceOnlySkillsSearch, [], args.instanceOnlySkillsSearch, [],
args.blurhash, not args.noapproval) not args.noapproval)

View File

@ -4,7 +4,7 @@ You will need python version 3.7 or later.
On a Debian based system: On a Debian based system:
sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget sudo apt install -y tor python3-socks imagemagick python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget
The following instructions install Epicyon to the /opt directory. It's not essential that it be installed there, and it could be in any other preferred directory. The following instructions install Epicyon to the /opt directory. It's not essential that it be installed there, and it could be in any other preferred directory.

View File

@ -6,9 +6,6 @@ __maintainer__ = "Bob Mottram"
__email__ = "bob@freedombone.net" __email__ = "bob@freedombone.net"
__status__ = "Production" __status__ = "Production"
from blurhash import blurhash_encode
from PIL import Image
import numpy
import os import os
import datetime import datetime
from hashlib import sha1 from hashlib import sha1
@ -56,11 +53,6 @@ def removeMetaData(imageFilename: str, outputFilename: str) -> None:
os.system('/usr/bin/mogrify -strip ' + outputFilename) # nosec os.system('/usr/bin/mogrify -strip ' + outputFilename) # nosec
def _getImageHash(imageFilename: str) -> str:
value = numpy.array(Image.open(imageFilename).convert("RGB"))
return blurhash_encode(value)
def _isMedia(imageFilename: str) -> bool: def _isMedia(imageFilename: str) -> bool:
permittedMedia = getMediaExtensions() permittedMedia = getMediaExtensions()
for m in permittedMedia: for m in permittedMedia:
@ -136,12 +128,9 @@ def _updateEtag(mediaFilename: str) -> None:
def attachMedia(baseDir: str, httpPrefix: str, domain: str, port: int, def attachMedia(baseDir: str, httpPrefix: str, domain: str, port: int,
postJson: {}, imageFilename: str, postJson: {}, imageFilename: str,
mediaType: str, description: str, mediaType: str, description: str) -> {}:
useBlurhash: bool) -> {}:
"""Attaches media to a json object post """Attaches media to a json object post
The description can be None The description can be None
Blurhash is optional, since low power systems may take a long
time to calculate it
""" """
if not _isMedia(imageFilename): if not _isMedia(imageFilename):
return postJson return postJson
@ -181,8 +170,6 @@ def attachMedia(baseDir: str, httpPrefix: str, domain: str, port: int,
} }
if mediaType.startswith('image/'): if mediaType.startswith('image/'):
attachmentJson['focialPoint'] = [0.0, 0.0] attachmentJson['focialPoint'] = [0.0, 0.0]
if useBlurhash:
attachmentJson['blurhash'] = _getImageHash(imageFilename)
postJson['attachment'] = [attachmentJson] postJson['attachment'] = [attachmentJson]
if baseDir: if baseDir:

View File

@ -542,7 +542,6 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str,
rssDescription = removeHtmlTag(rssDescription, 'height') rssDescription = removeHtmlTag(rssDescription, 'height')
followersOnly = False followersOnly = False
useBlurhash = False
# NOTE: the id when the post is created will not be # NOTE: the id when the post is created will not be
# consistent (it's based on the current time, not the # consistent (it's based on the current time, not the
# published time), so we change that later # published time), so we change that later
@ -550,7 +549,7 @@ def _convertRSStoActivityPub(baseDir: str, httpPrefix: str,
domain, port, httpPrefix, domain, port, httpPrefix,
rssDescription, rssDescription,
followersOnly, False, followersOnly, False,
None, None, None, useBlurhash, None, None, None,
rssTitle) rssTitle)
if not blog: if not blog:
continue continue

View File

@ -803,7 +803,7 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, attachImageFilename: str,
mediaType: str, imageDescription: str, mediaType: str, imageDescription: str,
useBlurhash: bool, isModerationReport: bool, isModerationReport: bool,
isArticle: bool, isArticle: bool,
inReplyTo=None, inReplyTo=None,
inReplyToAtomUri=None, subject=None, schedulePost=False, inReplyToAtomUri=None, subject=None, schedulePost=False,
@ -1034,7 +1034,7 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int,
newPost['object'] = \ newPost['object'] = \
attachMedia(baseDir, httpPrefix, domain, port, attachMedia(baseDir, httpPrefix, domain, port,
newPost['object'], attachImageFilename, newPost['object'], attachImageFilename,
mediaType, imageDescription, useBlurhash) mediaType, imageDescription)
_appendEventFields(newPost['object'], eventUUID, eventStatus, _appendEventFields(newPost['object'], eventUUID, eventStatus,
anonymousParticipationEnabled, anonymousParticipationEnabled,
repliesModerationOption, repliesModerationOption,
@ -1082,7 +1082,7 @@ def _createPostBase(baseDir: str, nickname: str, domain: str, port: int,
newPost = \ newPost = \
attachMedia(baseDir, httpPrefix, domain, port, attachMedia(baseDir, httpPrefix, domain, port,
newPost, attachImageFilename, newPost, attachImageFilename,
mediaType, imageDescription, useBlurhash) mediaType, imageDescription)
_appendEventFields(newPost, eventUUID, eventStatus, _appendEventFields(newPost, eventUUID, eventStatus,
anonymousParticipationEnabled, anonymousParticipationEnabled,
repliesModerationOption, repliesModerationOption,
@ -1225,7 +1225,7 @@ def createPublicPost(baseDir: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
inReplyTo=None, inReplyToAtomUri=None, subject=None, inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False, schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}: eventDate=None, eventTime=None, location=None) -> {}:
@ -1239,7 +1239,7 @@ def createPublicPost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location, schedulePost, eventDate, eventTime, location,
None, None, None, None, None, None, None, None, None, None,
@ -1251,7 +1251,7 @@ def createBlogPost(baseDir: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
inReplyTo=None, inReplyToAtomUri=None, subject=None, inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False, schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}: eventDate=None, eventTime=None, location=None) -> {}:
@ -1261,7 +1261,7 @@ def createBlogPost(baseDir: str,
content, followersOnly, saveToFile, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
inReplyTo, inReplyToAtomUri, subject, inReplyTo, inReplyToAtomUri, subject,
schedulePost, schedulePost,
eventDate, eventTime, location) eventDate, eventTime, location)
@ -1298,7 +1298,7 @@ def createNewsPost(baseDir: str,
domain: str, port: int, httpPrefix: str, domain: str, port: int, httpPrefix: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
subject: str) -> {}: subject: str) -> {}:
clientToServer = False clientToServer = False
inReplyTo = None inReplyTo = None
@ -1313,7 +1313,7 @@ def createNewsPost(baseDir: str,
content, followersOnly, saveToFile, content, followersOnly, saveToFile,
clientToServer, False, clientToServer, False,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
inReplyTo, inReplyToAtomUri, subject, inReplyTo, inReplyToAtomUri, subject,
schedulePost, schedulePost,
eventDate, eventTime, location) eventDate, eventTime, location)
@ -1327,7 +1327,7 @@ def createQuestionPost(baseDir: str,
followersOnly: bool, saveToFile: bool, followersOnly: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
subject: str, durationDays: int) -> {}: subject: str, durationDays: int) -> {}:
"""Question post with multiple choice options """Question post with multiple choice options
""" """
@ -1340,7 +1340,7 @@ def createQuestionPost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, False, None, None, subject, False, False, None, None, subject,
False, None, None, None, None, None, False, None, None, None, None, None,
None, None, None, None, None, None,
@ -1371,7 +1371,7 @@ def createUnlistedPost(baseDir: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
inReplyTo=None, inReplyToAtomUri=None, subject=None, inReplyTo=None, inReplyToAtomUri=None, subject=None,
schedulePost=False, schedulePost=False,
eventDate=None, eventTime=None, location=None) -> {}: eventDate=None, eventTime=None, location=None) -> {}:
@ -1385,7 +1385,7 @@ def createUnlistedPost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location, schedulePost, eventDate, eventTime, location,
None, None, None, None, None, None, None, None, None, None,
@ -1399,7 +1399,7 @@ def createFollowersOnlyPost(baseDir: str,
saveToFile: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
inReplyTo=None, inReplyToAtomUri=None, inReplyTo=None, inReplyToAtomUri=None,
subject=None, schedulePost=False, subject=None, schedulePost=False,
eventDate=None, eventTime=None, eventDate=None, eventTime=None,
@ -1414,7 +1414,7 @@ def createFollowersOnlyPost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location, schedulePost, eventDate, eventTime, location,
None, None, None, None, None, None, None, None, None, None,
@ -1428,7 +1428,7 @@ def createEventPost(baseDir: str,
saveToFile: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
subject=None, schedulePost=False, subject=None, schedulePost=False,
eventDate=None, eventTime=None, eventDate=None, eventTime=None,
location=None, category=None, joinMode=None, location=None, category=None, joinMode=None,
@ -1461,7 +1461,7 @@ def createEventPost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, False, None, None, subject, False, False, None, None, subject,
schedulePost, eventDate, eventTime, location, schedulePost, eventDate, eventTime, location,
eventUUID, category, joinMode, eventUUID, category, joinMode,
@ -1513,7 +1513,7 @@ def createDirectMessagePost(baseDir: str,
saveToFile: bool, clientToServer: bool, saveToFile: bool, clientToServer: bool,
commentsEnabled: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
inReplyTo=None, inReplyToAtomUri=None, inReplyTo=None, inReplyToAtomUri=None,
subject=None, debug=False, subject=None, debug=False,
schedulePost=False, schedulePost=False,
@ -1536,7 +1536,7 @@ def createDirectMessagePost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, False, inReplyTo, inReplyToAtomUri, subject, False, False, inReplyTo, inReplyToAtomUri, subject,
schedulePost, eventDate, eventTime, location, schedulePost, eventDate, eventTime, location,
None, None, None, None, None, None, None, None, None, None,
@ -1557,7 +1557,7 @@ def createReportPost(baseDir: str,
content: str, followersOnly: bool, saveToFile: bool, content: str, followersOnly: bool, saveToFile: bool,
clientToServer: bool, commentsEnabled: bool, clientToServer: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
debug: bool, subject=None) -> {}: debug: bool, subject=None) -> {}:
"""Send a report to moderators """Send a report to moderators
""" """
@ -1626,7 +1626,7 @@ def createReportPost(baseDir: str,
httpPrefix, content, followersOnly, saveToFile, httpPrefix, content, followersOnly, saveToFile,
clientToServer, commentsEnabled, clientToServer, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
True, False, None, None, subject, True, False, None, None, subject,
False, None, None, None, None, None, False, None, None, None, None, None,
None, None, None, None, None, None,
@ -1717,7 +1717,7 @@ def sendPost(projectVersion: str,
saveToFile: bool, clientToServer: bool, saveToFile: bool, clientToServer: bool,
commentsEnabled: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
federationList: [], sendThreads: [], postLog: [], federationList: [], sendThreads: [], postLog: [],
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
isArticle: bool, isArticle: bool,
@ -1776,7 +1776,7 @@ def sendPost(projectVersion: str,
followersOnly, saveToFile, clientToServer, followersOnly, saveToFile, clientToServer,
commentsEnabled, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, isArticle, inReplyTo, False, isArticle, inReplyTo,
inReplyToAtomUri, subject, inReplyToAtomUri, subject,
False, None, None, None, None, None, False, None, None, None, None, None,
@ -1838,7 +1838,7 @@ def sendPostViaServer(projectVersion: str,
httpPrefix: str, content: str, followersOnly: bool, httpPrefix: str, content: str, followersOnly: bool,
commentsEnabled: bool, commentsEnabled: bool,
attachImageFilename: str, mediaType: str, attachImageFilename: str, mediaType: str,
imageDescription: str, useBlurhash: bool, imageDescription: str,
cachedWebfingers: {}, personCache: {}, cachedWebfingers: {}, personCache: {},
isArticle: bool, debug=False, inReplyTo=None, isArticle: bool, debug=False, inReplyTo=None,
inReplyToAtomUri=None, subject=None) -> int: inReplyToAtomUri=None, subject=None) -> int:
@ -1914,7 +1914,7 @@ def sendPostViaServer(projectVersion: str,
followersOnly, saveToFile, clientToServer, followersOnly, saveToFile, clientToServer,
commentsEnabled, commentsEnabled,
attachImageFilename, mediaType, attachImageFilename, mediaType,
imageDescription, useBlurhash, imageDescription,
False, isArticle, inReplyTo, False, isArticle, inReplyTo,
inReplyToAtomUri, subject, inReplyToAtomUri, subject,
False, None, None, None, None, None, False, None, None, None, None, None,

View File

@ -256,7 +256,6 @@ def createServerAlice(path: str, domain: str, port: int,
httpPrefix = 'http' httpPrefix = 'http'
proxyType = None proxyType = None
password = 'alicepass' password = 'alicepass'
useBlurhash = True
maxReplies = 64 maxReplies = 64
domainMaxPostsPerDay = 1000 domainMaxPostsPerDay = 1000
accountMaxPostsPerDay = 1000 accountMaxPostsPerDay = 1000
@ -289,8 +288,7 @@ def createServerAlice(path: str, domain: str, port: int,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testMediaType,
testImageDescription, testImageDescription)
useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"Curiouser and curiouser!", "Curiouser and curiouser!",
testFollowersOnly, testFollowersOnly,
@ -299,8 +297,7 @@ def createServerAlice(path: str, domain: str, port: int,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testMediaType,
testImageDescription, testImageDescription)
useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"In the gardens of memory, in the palace " + "In the gardens of memory, in the palace " +
"of dreams, that is where you and I shall meet", "of dreams, that is where you and I shall meet",
@ -310,8 +307,7 @@ def createServerAlice(path: str, domain: str, port: int,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testMediaType,
testImageDescription, testImageDescription)
useBlurhash)
global testServerAliceRunning global testServerAliceRunning
testServerAliceRunning = True testServerAliceRunning = True
maxMentions = 10 maxMentions = 10
@ -338,7 +334,7 @@ def createServerAlice(path: str, domain: str, port: int,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
proxyType, maxReplies, proxyType, maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay, domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, True, True, False, sendThreads, False, allowDeletion, True, True, False, sendThreads,
False) False)
@ -356,7 +352,6 @@ def createServerBob(path: str, domain: str, port: int,
proxyType = None proxyType = None
clientToServer = False clientToServer = False
password = 'bobpass' password = 'bobpass'
useBlurhash = False
maxReplies = 64 maxReplies = 64
domainMaxPostsPerDay = 1000 domainMaxPostsPerDay = 1000
accountMaxPostsPerDay = 1000 accountMaxPostsPerDay = 1000
@ -388,8 +383,7 @@ def createServerBob(path: str, domain: str, port: int,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testMediaType,
testImageDescription, testImageDescription)
useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"One of the things I've realised is that " + "One of the things I've realised is that " +
"I am very simple", "I am very simple",
@ -399,8 +393,7 @@ def createServerBob(path: str, domain: str, port: int,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testMediaType,
testImageDescription, testImageDescription)
useBlurhash)
createPublicPost(path, nickname, domain, port, httpPrefix, createPublicPost(path, nickname, domain, port, httpPrefix,
"Quantum physics is a bit of a passion of mine", "Quantum physics is a bit of a passion of mine",
testFollowersOnly, testFollowersOnly,
@ -409,8 +402,7 @@ def createServerBob(path: str, domain: str, port: int,
testCommentsEnabled, testCommentsEnabled,
testAttachImageFilename, testAttachImageFilename,
testMediaType, testMediaType,
testImageDescription, testImageDescription)
useBlurhash)
global testServerBobRunning global testServerBobRunning
testServerBobRunning = True testServerBobRunning = True
maxMentions = 10 maxMentions = 10
@ -437,7 +429,7 @@ def createServerBob(path: str, domain: str, port: int,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
proxyType, maxReplies, proxyType, maxReplies,
domainMaxPostsPerDay, accountMaxPostsPerDay, domainMaxPostsPerDay, accountMaxPostsPerDay,
allowDeletion, True, True, False, sendThreads, False, allowDeletion, True, True, False, sendThreads,
False) False)
@ -485,7 +477,7 @@ def createServerEve(path: str, domain: str, port: int, federationList: [],
onionDomain, i2pDomain, None, port, port, onionDomain, i2pDomain, None, port, port,
httpPrefix, federationList, maxMentions, maxEmoji, False, httpPrefix, federationList, maxMentions, maxEmoji, False,
proxyType, maxReplies, allowDeletion, True, True, False, proxyType, maxReplies, allowDeletion, True, True, False,
sendThreads, False, False) sendThreads, False)
def testPostMessageBetweenServers(): def testPostMessageBetweenServers():
@ -574,7 +566,6 @@ def testPostMessageBetweenServers():
attachedImageFilename = baseDir + '/img/logo.png' attachedImageFilename = baseDir + '/img/logo.png'
mediaType = getAttachmentMediaType(attachedImageFilename) mediaType = getAttachmentMediaType(attachedImageFilename)
attachedImageDescription = 'Logo' attachedImageDescription = 'Logo'
useBlurhash = True
isArticle = False isArticle = False
# nothing in Alice's outbox # nothing in Alice's outbox
outboxPath = aliceDir + '/accounts/alice@' + aliceDomain + '/outbox' outboxPath = aliceDir + '/accounts/alice@' + aliceDomain + '/outbox'
@ -590,7 +581,7 @@ def testPostMessageBetweenServers():
followersOnly, followersOnly,
saveToFile, clientToServer, True, saveToFile, clientToServer, True,
attachedImageFilename, mediaType, attachedImageFilename, mediaType,
attachedImageDescription, useBlurhash, federationList, attachedImageDescription, federationList,
aliceSendThreads, alicePostLog, aliceCachedWebfingers, aliceSendThreads, alicePostLog, aliceCachedWebfingers,
alicePersonCache, isArticle, inReplyTo, alicePersonCache, isArticle, inReplyTo,
inReplyToAtomUri, subject) inReplyToAtomUri, subject)
@ -890,7 +881,6 @@ def testFollowBetweenServers():
alicePersonCache = {} alicePersonCache = {}
aliceCachedWebfingers = {} aliceCachedWebfingers = {}
alicePostLog = [] alicePostLog = []
useBlurhash = False
isArticle = False isArticle = False
sendResult = \ sendResult = \
sendPost(__version__, sendPost(__version__,
@ -898,7 +888,7 @@ def testFollowBetweenServers():
'bob', bobDomain, bobPort, ccUrl, 'bob', bobDomain, bobPort, ccUrl,
httpPrefix, 'Alice message', followersOnly, saveToFile, httpPrefix, 'Alice message', followersOnly, saveToFile,
clientToServer, True, clientToServer, True,
None, None, None, useBlurhash, federationList, None, None, None, federationList,
aliceSendThreads, alicePostLog, aliceCachedWebfingers, aliceSendThreads, alicePostLog, aliceCachedWebfingers,
alicePersonCache, isArticle, inReplyTo, alicePersonCache, isArticle, inReplyTo,
inReplyToAtomUri, subject) inReplyToAtomUri, subject)
@ -1178,7 +1168,6 @@ def testCreatePerson():
port = 80 port = 80
httpPrefix = 'https' httpPrefix = 'https'
clientToServer = False clientToServer = False
useBlurhash = False
baseDir = currDir + '/.tests_createperson' baseDir = currDir + '/.tests_createperson'
if os.path.isdir(baseDir): if os.path.isdir(baseDir):
shutil.rmtree(baseDir) shutil.rmtree(baseDir)
@ -1197,7 +1186,7 @@ def testCreatePerson():
archivePostsForPerson(nickname, domain, baseDir, 'outbox', None, {}, 4) archivePostsForPerson(nickname, domain, baseDir, 'outbox', None, {}, 4)
createPublicPost(baseDir, nickname, domain, port, httpPrefix, createPublicPost(baseDir, nickname, domain, port, httpPrefix,
"G'day world!", False, True, clientToServer, "G'day world!", False, True, clientToServer,
True, None, None, useBlurhash, None, None, True, None, None, None, None,
'Not suitable for Vogons') 'Not suitable for Vogons')
os.chdir(currDir) os.chdir(currDir)
@ -1401,7 +1390,6 @@ def testClientToServer():
attachedImageFilename = baseDir+'/img/logo.png' attachedImageFilename = baseDir+'/img/logo.png'
mediaType = getAttachmentMediaType(attachedImageFilename) mediaType = getAttachmentMediaType(attachedImageFilename)
attachedImageDescription = 'Logo' attachedImageDescription = 'Logo'
useBlurhash = False
isArticle = False isArticle = False
cachedWebfingers = {} cachedWebfingers = {}
personCache = {} personCache = {}
@ -1420,7 +1408,7 @@ def testClientToServer():
httpPrefix, 'Sent from my ActivityPub client', httpPrefix, 'Sent from my ActivityPub client',
followersOnly, True, followersOnly, True,
attachedImageFilename, mediaType, attachedImageFilename, mediaType,
attachedImageDescription, useBlurhash, attachedImageDescription,
cachedWebfingers, personCache, isArticle, cachedWebfingers, personCache, isArticle,
True, None, None, None) True, None, None, None)
print('sendResult: ' + str(sendResult)) print('sendResult: ' + str(sendResult))

View File

@ -1267,7 +1267,7 @@
<p class="intro">You will need python version 3.7 or later.</p> <p class="intro">You will need python version 3.7 or later.</p>
<p class="intro">On a Debian based system:</p> <p class="intro">On a Debian based system:</p>
<div class="shell"> <div class="shell">
<p>sudo apt install -y tor python3-socks imagemagick python3-numpy python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-pil.imagetk python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget</p> <p>sudo apt install -y tor python3-socks imagemagick python3-setuptools python3-crypto python3-pycryptodome python3-dateutil python3-idna python3-requests python3-flake8 python3-django-timezone-field python3-pyqrcode python3-png python3-bandit libimage-exiftool-perl certbot nginx wget</p>
</div> </div>
<p class="intro"> <p class="intro">