Rewrite it in Rust

For the curious: this wasn't completely for no reason, although it
appears that way from the outside. I deploy this code to AWS via the
CDK, and the Python Lambda function bundler in the CDK has two unwanted
properties:

1. The assets produced by the bundler are not reproducible, due to the
   timestamps stored in the .pyc files. The bundler does nothing to
   assist with this.
2. A bug (that was reported, and that the CDK maintainers converted from
   an issue into discussion without any investigation) prevents the use
   of arm64 runtimes with this bundler. This requires emulating x86_64
   on my development machines, which are now only ARM.

... whereas the Rust function bundler I wrote does not have these
problems. So.
This commit is contained in:
iliana etaoin 2022-06-24 21:16:22 -07:00
parent 7ed4c03bf3
commit 67b8bd9432
16 changed files with 2585 additions and 441 deletions

1
.dockerignore Symbolic link
View File

@ -0,0 +1 @@
.gitignore

View File

@ -1,3 +0,0 @@
[flake8]
max-line-length = 88
extend-ignore = E203

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/target

View File

@ -1,7 +0,0 @@
[settings]
multi_line_output = 3
include_trailing_comma = True
force_grid_wrap = 0
use_parentheses = True
ensure_newline_before_comments = True
line_length = 88

2260
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

22
Cargo.toml Normal file
View File

@ -0,0 +1,22 @@
[package]
name = "emojos-dot-in"
version = "2.0.0"
edition = "2021"
license = "AGPL-3.0-or-later"
description = "Shows custom emoji for Mastodon/Pleroma instances"
homepage = "https://emojos.in"
repository = "https://github.com/iliana/emojos.in"
publish = false
[dependencies]
anyhow = "1"
askama = "0.11"
lambda_http = "0.5"
reqwest = { version = "0.11", features = ["gzip", "brotli", "deflate", "json"] }
rocket = { version = "0.5.0-rc.2", default-features = false }
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["rt-multi-thread"] }
[build-dependencies]
anyhow = "1"
zip = { version = "0.6", default-features = false, features = ["deflate"] }

18
Pipfile
View File

@ -1,18 +0,0 @@
[[source]]
url = "https://pypi.python.org/simple"
verify_ssl = true
name = "pypi"
[packages]
flask = "~=2.1.2"
requests = "~=2.28.0"
serverless-wsgi = "~=3.0.0"
[dev-packages]
black = "*"
botocore = "==1.23.32"
flake8 = "*"
isort = "*"
[requires]
python_version = "3.9"

302
Pipfile.lock generated
View File

@ -1,302 +0,0 @@
{
"_meta": {
"hash": {
"sha256": "c23c0e8860e8b957ce01c6c6cc2ab6ff85b1d0535e9bdd12e27cbe93be7668dc"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.9"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.python.org/simple",
"verify_ssl": true
}
]
},
"default": {
"certifi": {
"hashes": [
"sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d",
"sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"
],
"markers": "python_version >= '3.6'",
"version": "==2022.6.15"
},
"charset-normalizer": {
"hashes": [
"sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597",
"sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"
],
"markers": "python_full_version >= '3.5.0'",
"version": "==2.0.12"
},
"click": {
"hashes": [
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.3"
},
"flask": {
"hashes": [
"sha256:315ded2ddf8a6281567edb27393010fe3406188bafbfe65a3339d5787d89e477",
"sha256:fad5b446feb0d6db6aec0c3184d16a8c1f6c3e464b511649c8918a9be100b4fe"
],
"index": "pypi",
"version": "==2.1.2"
},
"idna": {
"hashes": [
"sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff",
"sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"
],
"markers": "python_full_version >= '3.5.0'",
"version": "==3.3"
},
"itsdangerous": {
"hashes": [
"sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44",
"sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
},
"jinja2": {
"hashes": [
"sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852",
"sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"
],
"markers": "python_version >= '3.7'",
"version": "==3.1.2"
},
"markupsafe": {
"hashes": [
"sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003",
"sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88",
"sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5",
"sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7",
"sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a",
"sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603",
"sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1",
"sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135",
"sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247",
"sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6",
"sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601",
"sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77",
"sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02",
"sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e",
"sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63",
"sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f",
"sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980",
"sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b",
"sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812",
"sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff",
"sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96",
"sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1",
"sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925",
"sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a",
"sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6",
"sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e",
"sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f",
"sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4",
"sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f",
"sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3",
"sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c",
"sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a",
"sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417",
"sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a",
"sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a",
"sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37",
"sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452",
"sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933",
"sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a",
"sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.1"
},
"requests": {
"hashes": [
"sha256:bc7861137fbce630f17b03d3ad02ad0bf978c844f3536d0edda6499dafce2b6f",
"sha256:d568723a7ebd25875d8d1eaf5dfa068cd2fc8194b2e483d7b1f7c81918dbec6b"
],
"index": "pypi",
"version": "==2.28.0"
},
"serverless-wsgi": {
"hashes": [
"sha256:2bb42fbb23eb8f0ccfc5666c7b6fc6af36f9948336dbb99d033416447e5a1d4c",
"sha256:e5d699dfefc3e593c11a07b9af2d89cdccd3110d9a1ee28c83a9fcf301c6fcbc"
],
"index": "pypi",
"version": "==3.0.0"
},
"urllib3": {
"hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.9"
},
"werkzeug": {
"hashes": [
"sha256:1ce08e8093ed67d638d63879fd1ba3735817f7a80de3674d293f5984f25fb6e6",
"sha256:72a4b735692dd3135217911cbeaa1be5fa3f62bffb8745c5215420a03dc55255"
],
"markers": "python_version >= '3.7'",
"version": "==2.1.2"
}
},
"develop": {
"black": {
"hashes": [
"sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b",
"sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176",
"sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09",
"sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a",
"sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015",
"sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79",
"sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb",
"sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20",
"sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464",
"sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968",
"sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82",
"sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21",
"sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0",
"sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265",
"sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b",
"sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a",
"sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72",
"sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce",
"sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0",
"sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a",
"sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163",
"sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad",
"sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"
],
"index": "pypi",
"version": "==22.3.0"
},
"botocore": {
"hashes": [
"sha256:49c39a4e0ece6e486aefa23d7c96c5f9a4c41a6ceab3cc41b157e8b5bea749e3",
"sha256:a33e6b30c5d64156d10ec6986fc96dfdf5c723ad7dc83e0d60af6407192b16b4"
],
"index": "pypi",
"version": "==1.23.32"
},
"click": {
"hashes": [
"sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e",
"sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"
],
"markers": "python_version >= '3.7'",
"version": "==8.1.3"
},
"flake8": {
"hashes": [
"sha256:479b1304f72536a55948cb40a32dce8bb0ffe3501e26eaf292c7e60eb5e0428d",
"sha256:806e034dda44114815e23c16ef92f95c91e4c71100ff52813adf7132a6ad870d"
],
"index": "pypi",
"version": "==4.0.1"
},
"isort": {
"hashes": [
"sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7",
"sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"
],
"index": "pypi",
"version": "==5.10.1"
},
"jmespath": {
"hashes": [
"sha256:b85d0567b8666149a93172712e68920734333c0ce7e89b78b3e987f71e5ed4f9",
"sha256:cdf6525904cc597730141d61b36f2e4b8ecc257c420fa2f4549bac2c2d0cb72f"
],
"markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==0.10.0"
},
"mccabe": {
"hashes": [
"sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
"sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
],
"version": "==0.6.1"
},
"mypy-extensions": {
"hashes": [
"sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
"sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
],
"version": "==0.4.3"
},
"pathspec": {
"hashes": [
"sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a",
"sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"
],
"version": "==0.9.0"
},
"platformdirs": {
"hashes": [
"sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788",
"sha256:58c8abb07dcb441e6ee4b11d8df0ac856038f944ab98b7be6b27b2a3c7feef19"
],
"markers": "python_version >= '3.7'",
"version": "==2.5.2"
},
"pycodestyle": {
"hashes": [
"sha256:720f8b39dde8b293825e7ff02c475f3077124006db4f440dcbc9a20b76548a20",
"sha256:eddd5847ef438ea1c7870ca7eb78a9d47ce0cdb4851a5523949f2601d0cbbe7f"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
"version": "==2.8.0"
},
"pyflakes": {
"hashes": [
"sha256:05a85c2872edf37a4ed30b0cce2f6093e1d0581f8c19d7393122da7e25b2b24c",
"sha256:3bb3a3f256f4b7968c9c788781e4ff07dce46bdf12339dcda61053375426ee2e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.4.0"
},
"python-dateutil": {
"hashes": [
"sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
"sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==2.8.2"
},
"six": {
"hashes": [
"sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
"sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
"version": "==1.16.0"
},
"tomli": {
"hashes": [
"sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc",
"sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"
],
"markers": "python_version < '3.11'",
"version": "==2.0.1"
},
"urllib3": {
"hashes": [
"sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14",
"sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"
],
"markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
"version": "==1.26.9"
}
}
}

99
app.py
View File

@ -1,99 +0,0 @@
# Copyright (c) 2021 iliana etaoin
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import operator
import urllib.parse
import botocore.session
import requests
import serverless_wsgi
from flask import Flask, redirect, render_template, request, url_for
app = Flask(__name__)
@app.route("/<domain>")
def emojo(domain):
if request.args.get("show_all", "") == "on":
show_all = True
else:
show_all = False
if request.args.get("show_animated", "") == "on":
show_animated = True
else:
show_animated = False
try:
url = urllib.parse.urlunsplit(
("https", domain, "/api/v1/custom_emojis", "", "")
)
response = requests.get(url)
if response.status_code == 401:
return render_template("forbidden.html", domain=domain)
if show_all:
emojo = sorted(response.json(), key=operator.itemgetter("shortcode"))
else:
emojo = sorted(
filter(lambda x: x.get("visible_in_picker", True), response.json()),
key=operator.itemgetter("shortcode"),
)
return render_template(
"emojo.html", domain=domain, emojo=emojo, show_animated=show_animated
)
except requests.exceptions.RequestException:
return render_template("oh_no.html", domain=domain)
@app.route("/favicon.ico")
@app.route("/robots.txt")
def no_content():
return ("", 204)
@app.route("/code")
def code():
context = request.environ.get("context")
session = botocore.session.get_session()
# region name is detected from lambda environment
client = session.create_client("lambda")
code = client.get_function(
FunctionName=context.function_name, Qualifier=context.function_version
)
return redirect(code["Code"]["Location"], code=303)
@app.route("/", methods=("GET", "POST"))
def index():
if request.method == "POST":
if "instance" in request.form:
show_all = request.form.get("show_all")
show_animated = request.form.get("show_animated")
return redirect(
url_for(
"emojo",
domain=request.form["instance"],
show_all=show_all,
show_animated=show_animated,
)
)
else:
return redirect(url_for("index"))
else:
return render_template("index.html")
def handle_request(event, context):
return serverless_wsgi.handle_request(app, event, context)

38
build.rs Normal file
View File

@ -0,0 +1,38 @@
use anyhow::{ensure, Result};
use std::fs::File;
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::Command;
use zip::ZipWriter;
fn main() {
let out_dir = PathBuf::from(std::env::var_os("OUT_DIR").unwrap());
let mut rs_file = File::create(out_dir.join("zip.rs")).unwrap();
match attempt(&out_dir) {
Ok(()) => writeln!(rs_file, r#"Some(include_bytes!("source.zip"))"#).unwrap(),
Err(_) => writeln!(rs_file, "None").unwrap(),
}
}
fn attempt(out_dir: &Path) -> Result<()> {
let output = Command::new("cargo")
.args(["package", "--list", "--allow-dirty"])
.output()?;
ensure!(output.status.success(), "unsuccessful");
let mut writer = ZipWriter::new(File::create(out_dir.join("source.zip"))?);
for path in String::from_utf8(output.stdout)?.lines() {
if path == "Cargo.toml.orig" {
continue;
}
writer.start_file(path, Default::default())?;
io::copy(
&mut File::open(Path::new(env!("CARGO_MANIFEST_DIR")).join(path))?,
&mut writer,
)?;
}
writer.finish()?;
Ok(())
}

178
src/app.rs Normal file
View File

@ -0,0 +1,178 @@
use askama::Template;
use reqwest::{Client, StatusCode, Url};
use rocket::form::{Form, FromForm};
use rocket::http::{ContentType, Header};
use rocket::response::status::NoContent;
use rocket::response::{Debug, Redirect, Responder};
use rocket::{get, post, routes, uri, Build, Rocket, State};
use serde::Deserialize;
use std::str::FromStr;
pub fn rocket() -> Rocket<Build> {
rocket::build()
.manage(
Client::builder()
.user_agent(format!(
"emojos.in/{} (+https://github.com/iliana/emojos.in)",
env!("CARGO_PKG_VERSION")
))
.build()
.unwrap(),
)
.mount(
"/",
routes![
code,
copy_js,
css,
favicon_ico,
index,
instance,
instance_form,
robots_txt
],
)
}
#[get("/")]
fn index() -> Result<(ContentType, String), Debug<askama::Error>> {
#[derive(Template)]
#[template(path = "index.html")]
struct Index;
Ok((ContentType::HTML, Index.render()?))
}
#[derive(FromForm)]
struct InstanceForm<'a> {
instance: &'a str,
show_all: bool,
show_animated: bool,
}
#[post("/", data = "<form>")]
fn instance_form(form: Form<InstanceForm<'_>>) -> Redirect {
Redirect::to(uri!(instance(
form.instance,
form.show_all.then(|| true),
form.show_animated.then(|| true),
)))
}
#[get("/<instance>?<show_all>&<show_animated>")]
async fn instance(
client: &State<Client>,
instance: &str,
show_all: Option<bool>,
show_animated: Option<bool>,
) -> Result<(ContentType, String), Debug<anyhow::Error>> {
#[derive(Deserialize)]
struct Emojo {
shortcode: String,
url: String,
static_url: String,
visible_in_picker: bool,
}
#[derive(Template)]
#[template(path = "emojo.html")]
struct Output<'a> {
instance: &'a str,
show_animated: bool,
emojo: Vec<Emojo>,
}
#[derive(Template)]
#[template(path = "oh_no.html")]
struct OhNo<'a> {
instance: &'a str,
status: StatusCode,
why: &'a str,
}
let show_all = show_all.unwrap_or_default();
let show_animated = show_animated.unwrap_or_default();
let output = async {
let mut url = Url::from_str("https://host.invalid/api/v1/custom_emojis").unwrap();
url.set_host(Some(instance))?;
let response = client.get(url).send().await?;
if response.status().is_client_error() || response.status().is_server_error() {
return Ok(OhNo {
instance,
status: response.status(),
why: match response.status() {
StatusCode::FORBIDDEN => "This instance's emoji list is private.",
StatusCode::NOT_FOUND => {
"This instance doesn't support the Mastodon custom emoji API."
}
_ => "That's all we know.",
},
}
.render()?);
}
let mut emojo: Vec<Emojo> = response.json().await?;
if !show_all {
emojo = emojo.into_iter().filter(|x| x.visible_in_picker).collect();
}
anyhow::Ok(
Output {
instance,
show_animated,
emojo,
}
.render()?,
)
}
.await?;
Ok((ContentType::HTML, output))
}
#[derive(Responder)]
struct Code {
zip: &'static [u8],
content_type: ContentType,
disposition: Header<'static>,
}
#[get("/code")]
fn code() -> Option<Code> {
let zip = include!(concat!(env!("OUT_DIR"), "/zip.rs"))?;
Some(Code {
zip,
content_type: ContentType::ZIP,
disposition: Header::new(
"content-disposition",
r#"attachment; filename="emojos.in.zip""#,
),
})
}
#[get("/static/site.css")]
fn css() -> (ContentType, &'static str) {
(
ContentType::CSS,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/static/site.css")),
)
}
#[get("/static/copy.js")]
fn copy_js() -> (ContentType, &'static str) {
(
ContentType::JavaScript,
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/static/copy.js")),
)
}
#[get("/favicon.ico")]
fn favicon_ico() -> NoContent {
NoContent
}
#[get("/robots.txt")]
fn robots_txt() -> NoContent {
NoContent
}

75
src/main.rs Normal file
View File

@ -0,0 +1,75 @@
// Copyright (c) 2022 iliana etaoin
// SPDX-License-Identifier: AGPL-3.0-or-later
#![warn(clippy::pedantic)]
#![allow(
clippy::map_unwrap_or,
clippy::needless_pass_by_value,
clippy::no_effect_underscore_binding
)]
mod app;
use anyhow::{anyhow, Context, Result};
use lambda_http::{http, service_fn, Body};
use rocket::http::Header;
use rocket::local::asynchronous::Client;
fn main() {
// Rocket needs its own async runtime, but that runtime doesn't support the !Send lambda_http
// future.
if std::env::var_os("AWS_LAMBDA_RUNTIME_API").is_some() {
tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
let client = Client::untracked(app::rocket()).await.unwrap();
lambda_http::run(service_fn(|request| async {
Ok(handler(request, &client).await?)
}))
.await
.unwrap();
});
} else {
rocket::execute(async {
app::rocket().launch().await.ok();
});
}
}
async fn handler(request: http::Request<Body>, client: &Client) -> Result<http::Response<Body>> {
let mut local_request = client.req(
request
.method()
.to_string()
.parse()
.map_err(|()| anyhow!("invalid method"))?,
request
.uri()
.path_and_query()
.context("request has no path")?
.as_str(),
);
for (name, value) in request.headers() {
local_request.add_header(Header::new(
name.as_str().to_owned(),
value.to_str()?.to_owned(),
));
}
let local_response = local_request.body(request.body()).dispatch().await;
let mut response = http::Response::builder().status(local_response.status().code);
for header in local_response.headers().iter() {
let name: &str = header.name.as_ref();
let value: &str = header.value.as_ref();
response = response.header(name, value);
}
Ok(response.body(
local_response
.into_bytes()
.await
.map(Body::Binary)
.unwrap_or(Body::Empty),
)?)
}

View File

@ -2,7 +2,7 @@
<html>
<head>
<title>{% block title %}{% endblock title %}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='site.css') }}">
<link rel="stylesheet" href="/static/site.css">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
@ -14,7 +14,7 @@
&middot;
AGPLv3
&middot;
<a href="{{ url_for('code') }}">source code download</a>
<a href="/code">source code download</a>
&middot;
<a href="https://github.com/iliana/emojos.in">github</a>
</p>

View File

@ -1,8 +1,8 @@
{% extends "base.html" %}
{% block title %}Custom emoji list for {{ domain }}{% endblock title %}
{% block title %}Custom emoji list for {{ instance }}{% endblock title %}
{% block body %}
<p>
<b>{{ domain }}</b> emojo list<br>
<b>{{ instance }}</b> emojo list<br>
click/touch to copy to clipboard
</p>
<dl class="emojo">
@ -24,5 +24,5 @@
</dl>
{% endblock body %}
{% block script %}
<script src="{{ url_for('static', filename='copy.js') }}"></script>
<script src="/static/copy.js"></script>
{% endblock script %}

View File

@ -1,5 +0,0 @@
{% extends "base.html" %}
{% block title %}Custom emoji list for {{ domain }}{% endblock title %}
{% block body %}
<p><b>{{ domain }}</b> doesn't allow access to the <code>v1/custom_emojis</code> endpoint. :(</p>
{% endblock body %}

View File

@ -1,5 +1,8 @@
{% extends "base.html" %}
{% block title %}Custom emoji list for {{ domain }}{% endblock title %}
{% block title %}Custom emoji list for {{ instance }}{% endblock title %}
{% block body %}
<p><b>{{ domain }}</b> isn't a fediverse instance or doesn't have a <code>v1/custom_emojis</code> endpoint. :(</p>
<p>
<b>{{ instance }}</b>'s custom emoji API (/api/v1/custom_emojis) returned {{ status }}.
{{ why }} :(
</p>
{% endblock body %}