'federation',
'fba',
'instances',
+ 'network',
]
from fba import config
from fba import fba
from fba import instances
+from fba import network
from fba.federation import *
# DEBUG: print(f"DEBUG: args[]={type(args)} - CALLED!")
domains = list()
try:
- fetched = fba.post_json_api("gql.api.bka.li", "/v1/graphql", json.dumps({
+ fetched = network.post_json_api("gql.api.bka.li", "/v1/graphql", json.dumps({
"query": "query domainlist {nodeinfo(order_by: {domain: asc}) {domain}}"
}))
print(f"INFO: blocker='{blocker}',software='{software}'")
try:
if software == "friendica":
- json = fba.fetch_friendica_blocks(blocker)
+ json = friendica.fetch_blocks(blocker)
elif software == "misskey":
json = misskey.fetch_blocks(blocker)
print("WARNING: Unknown software:", blocker, software)
if config.get("bot_enabled") and len(blockdict) > 0:
- send_bot_post(blocker, blockdict)
+ network.send_bot_post(blocker, blockdict)
blockdict = []
try:
doc = bs4.BeautifulSoup(
- fba.get_response("meta.chaos.social", "/federation", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
+ network.fetch_response("meta.chaos.social", "/federation", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
"html.parser",
)
# DEBUG: print(f"DEBUG: doc()={len(doc)}[]={type(doc)}")
-# Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
# Copyright (C) 2023 Free Software Foundation
#
# This program is free software: you can redistribute it and/or modify
import bs4
import hashlib
import re
-import reqto
import requests
import json
import sqlite3
from fba import cache
from fba import config
from fba import instances
+from fba import network
from fba.federation import lemmy
from fba.federation import misskey
# DEBUG: print(f"DEBUG: Fetching peers from '{domain}',software='{software}' ...")
peers = list()
try:
- response = get_response(domain, "/api/v1/instance/peers", api_headers, (config.get("connection_timeout"), config.get("read_timeout")))
+ response = network.fetch_response(domain, "/api/v1/instance/peers", api_headers, (config.get("connection_timeout"), config.get("read_timeout")))
data = json_from_response(response)
# DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},data[]='{type(data)}'")
if not response.ok or response.status_code >= 400:
# DEBUG: print(f"DEBUG: Was not able to fetch peers, trying alternative ...")
- response = get_response(domain, "/api/v3/site", api_headers, (config.get("connection_timeout"), config.get("read_timeout")))
+ response = network.fetch_response(domain, "/api/v3/site", api_headers, (config.get("connection_timeout"), config.get("read_timeout")))
data = json_from_response(response)
# DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},data[]='{type(data)}'")
# DEBUG: print("DEBUG: Returning peers[]:", type(peers))
return peers
-def post_json_api(domain: str, path: str, parameter: str, extra_headers: dict = {}) -> dict:
- # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',parameter='{parameter}',extra_headers()={len(extra_headers)} - CALLED!")
- if type(domain) != str:
- raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
- elif domain == "":
- raise ValueError(f"Parameter 'domain' is empty")
- elif type(path) != str:
- raise ValueError(f"path[]={type(path)} is not 'str'")
- elif path == "":
- raise ValueError("Parameter 'path' cannot be empty")
- elif type(parameter) != str:
- raise ValueError(f"parameter[]={type(parameter)} is not 'str'")
-
- # DEBUG: print("DEBUG: Sending POST to domain,path,parameter:", domain, path, parameter, extra_headers)
- data = {}
- try:
- response = reqto.post(
- f"https://{domain}{path}",
- data=parameter,
- headers={**api_headers, **extra_headers},
- timeout=(config.get("connection_timeout"), config.get("read_timeout"))
- )
-
- data = json_from_response(response)
- # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},data[]='{type(data)}'")
- if not response.ok or response.status_code >= 400:
- print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',parameter()={len(parameter)},response.status_code='{response.status_code}',data[]='{type(data)}'")
- instances.update_last_error(domain, response)
-
- except BaseException as e:
- print(f"WARNING: Some error during post(): domain='{domain}',path='{path}',parameter()={len(parameter)},exception[{type(e)}]:'{str(e)}'")
-
- # DEBUG: print(f"DEBUG: Returning data({len(data)})=[]:{type(data)}")
- return data
-
def fetch_nodeinfo(domain: str, path: str = None) -> list:
# DEBUG: print(f"DEBUG: domain='{domain}',path={path} - CALLED!")
if type(domain) != str:
try:
# DEBUG: print(f"DEBUG: Fetching request='{request}' from domain='{domain}' ...")
- response = get_response(domain, request, api_headers, (config.get("nodeinfo_connection_timeout"), config.get("nodeinfo_read_timeout")))
+ response = network.fetch_response(domain, request, api_headers, (config.get("nodeinfo_connection_timeout"), config.get("nodeinfo_read_timeout")))
data = json_from_response(response)
# DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},data[]='{type(data)}'")
data = {}
try:
- response = get_response(domain, "/.well-known/nodeinfo", api_headers, (config.get("nodeinfo_connection_timeout"), config.get("nodeinfo_read_timeout")))
+ response = network.fetch_response(domain, "/.well-known/nodeinfo", api_headers, (config.get("nodeinfo_connection_timeout"), config.get("nodeinfo_read_timeout")))
data = json_from_response(response)
# DEBUG: print("DEBUG: domain,response.ok,data[]:", domain, response.ok, type(data))
try:
# DEBUG: print(f"DEBUG: Fetching path='{path}' from '{domain}' ...")
- response = get_response(domain, path, headers, (config.get("connection_timeout"), config.get("read_timeout")))
+ response = network.fetch_response(domain, path, headers, (config.get("connection_timeout"), config.get("read_timeout")))
# DEBUG: print("DEBUG: domain,response.ok,response.status_code,response.text[]:", domain, response.ok, response.status_code, type(response.text))
if response.ok and response.status_code < 300 and len(response.text) > 0:
# DEBUG: print("DEBUG: Returning domain,software:", domain, software)
return software
-def send_bot_post(instance: str, blocklist: dict):
- # DEBUG: print(f"DEBUG: instance={instance},blocklist()={len(blocklist)} - CALLED!")
- if type(domain) != str:
- raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
- elif domain == "":
- raise ValueError("Parameter 'domain' is empty")
- elif type(blocklist) != dict:
- raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not 'dict'")
-
- message = instance + " has blocked the following instances:\n\n"
- truncated = False
-
- if len(blocklist) > 20:
- truncated = True
- blocklist = blocklist[0 : 19]
-
- # DEBUG: print(f"DEBUG: blocklist()={len(blocklist)}")
- for block in blocklist:
- # DEBUG: print(f"DEBUG: block['{type(block)}']={block}")
- if block["reason"] == None or block["reason"] == '':
- message = message + block["blocked"] + " with unspecified reason\n"
- else:
- if len(block["reason"]) > 420:
- block["reason"] = block["reason"][0:419] + "[…]"
-
- message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
-
- if truncated:
- message = message + "(the list has been truncated to the first 20 entries)"
-
- botheaders = {**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}}
-
- req = reqto.post(
- f"{config.get('bot_instance')}/api/v1/statuses",
- data={
- "status" : message,
- "visibility" : config.get('bot_visibility'),
- "content_type": "text/plain"
- },
- headers=botheaders,
- timeout=10
- ).json()
-
- return True
-
-def fetch_friendica_blocks(domain: str) -> dict:
- # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
- if type(domain) != str:
- raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
- elif domain == "":
- raise ValueError(f"Parameter 'domain' is empty")
-
- # DEBUG: print("DEBUG: Fetching friendica blocks from domain:", domain)
- blocked = list()
-
- try:
- doc = bs4.BeautifulSoup(
- get_response(domain, "/friendica", headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
- "html.parser",
- )
- except BaseException as e:
- print("WARNING: Failed to fetch /friendica from domain:", domain, e)
- instances.update_last_error(domain, e)
- return {}
-
- blocklist = doc.find(id="about_blocklist")
-
- # Prevents exceptions:
- if blocklist is None:
- # DEBUG: print("DEBUG: Instance has no block list:", domain)
- return {}
-
- table = blocklist.find("table")
-
- # DEBUG: print(f"DEBUG: table[]='{type(table)}'")
- if table.find("tbody"):
- rows = table.find("tbody").find_all("tr")
- else:
- rows = table.find_all("tr")
-
- # DEBUG: print(f"DEBUG: Found rows()={len(rows)}")
- for line in rows:
- # DEBUG: print(f"DEBUG: line='{line}'")
- blocked.append({
- "domain": tidyup_domain(line.find_all("td")[0].text),
- "reason": tidyup_reason(line.find_all("td")[1].text)
- })
- # DEBUG: print("DEBUG: Next!")
-
- # DEBUG: print("DEBUG: Returning blocklist() for domain:", domain, len(blocklist))
- return {
- "reject": blocked
- }
-
def tidyup_reason(reason: str) -> str:
# DEBUG: print(f"DEBUG: reason='{reason}' - CALLED!")
if type(reason) != str:
# DEBUG: print(f"DEBUG: data[]={type(data)} - EXIT!")
return data
-def get_response(domain: str, path: str, headers: dict, timeout: list) -> requests.models.Response:
- # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',headers()={len(headers)},timeout={timeout} - CALLED!")
- if type(domain) != str:
- raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
- elif domain == "":
- raise ValueError("Parameter 'domain' is empty")
- elif type(path) != str:
- raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
- elif path == "":
- raise ValueError("Parameter 'path' is empty")
-
- try:
- # DEBUG: print(f"DEBUG: Sending request to '{domain}{path}' ...")
- response = reqto.get(
- f"https://{domain}{path}",
- headers=headers,
- timeout=timeout
- );
- except requests.exceptions.ConnectionError as e:
- # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(e)}]='{str(e)}'")
- instances.update_last_error(domain, e)
- raise e
-
- # DEBUG: print(f"DEBUG: response[]='{type(response)}' - EXXIT!")
- return response
-
def has_key(keys: list, search: str, value: any) -> bool:
# DEBUG: print(f"DEBUG: keys()={len(keys)},search='{search}',value[]='{type(value)}' - CALLED!")
if type(keys) != list:
# Invoke other function, avoid trailing ?
# DEBUG: print(f"DEBUG: components[{type(components)}]={components}")
if components.query != "":
- response = get_response(components.hostname, f"{components.path}?{components.query}", headers, timeout)
+ response = network.fetch_response(components.hostname, f"{components.path}?{components.query}", headers, timeout)
else:
- response = get_response(components.hostname, f"{components.path}", headers, timeout)
+ response = network.fetch_response(components.hostname, f"{components.path}", headers, timeout)
# DEBUG: print(f"DEBUG: response[]='{type(response)}' - EXXIT!")
return response
from fba import config
from fba import fba
from fba import instances
+from fba import network
def fetch_peers(domain: str) -> list:
# DEBUG: print(f"DEBUG: domain({len(domain)})={domain},software='lemmy' - CALLED!")
peers = list()
try:
# DEBUG: print(f"DEBUG: domain='{domain}' is Lemmy, fetching JSON ...")
- response = fba.get_response(domain, "/api/v3/site", fba.api_headers, (config.get("connection_timeout"), config.get("read_timeout")))
+ response = network.fetch_response(domain, "/api/v3/site", fba.api_headers, (config.get("connection_timeout"), config.get("read_timeout")))
data = fba.json_from_response(response)
from fba import config
from fba import fba
from fba import instances
+from fba import network
language_mapping = {
# English -> English
try:
doc = bs4.BeautifulSoup(
- fba.get_response(domain, "/about/more", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
+ network.fetch_response(domain, "/about/more", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
"html.parser",
)
except BaseException as e:
# handling CSRF, I've saw at least one server requiring it to access the endpoint
# DEBUG: print("DEBUG: Fetching meta:", domain)
meta = bs4.BeautifulSoup(
- fba.get_response(domain, "/", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
+ network.fetch_response(domain, "/", fba.headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
"html.parser",
)
try:
reqheaders = fba.api_headers
# DEBUG: print("DEBUG: Querying API domain_blocks:", domain)
- blocklist = fba.get_response(domain, "/api/v1/instance/domain_blocks", reqheaders, (config.get("connection_timeout"), config.get("read_timeout"))).json()
+ blocklist = network.fetch_response(domain, "/api/v1/instance/domain_blocks", reqheaders, (config.get("connection_timeout"), config.get("read_timeout"))).json()
print(f"INFO: Checking {len(blocklist)} entries from domain='{domain}',software='mastodon' ...")
for block in blocklist:
from fba import config
from fba import fba
from fba import instances
+from fba import network
def fetch_peers(domain: str) -> list:
# DEBUG: print(f"DEBUG: domain({len(domain)})={domain} - CALLED!")
while True:
# DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
if offset == 0:
- fetched = fba.post_json_api(domain, "/api/federation/instances", json.dumps({
+ fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
"sort" : "+pubAt",
"host" : None,
"limit": step
"Origin": domain
})
else:
- fetched = fba.post_json_api(domain, "/api/federation/instances", json.dumps({
+ fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
"sort" : "+pubAt",
"host" : None,
"limit" : step,
# DEBUG: print(f"DEBUG: Fetching offset='{offset}' from '{domain}' ...")
if offset == 0:
# DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
- fetched = fba.post_json_api(domain, "/api/federation/instances", json.dumps({
+ fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
"sort" : "+pubAt",
"host" : None,
"suspended": True,
})
else:
# DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
- fetched = fba.post_json_api(domain, "/api/federation/instances", json.dumps({
+ fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
"sort" : "+pubAt",
"host" : None,
"suspended": True,
try:
if offset == 0:
# DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
- fetched = fba.post_json_api(domain, "/api/federation/instances", json.dumps({
+ fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
"sort" : "+pubAt",
"host" : None,
"blocked": True,
})
else:
# DEBUG: print("DEBUG: Sending JSON API request to domain,step,offset:", domain, step, offset)
- fetched = fba.post_json_api(domain, "/api/federation/instances", json.dumps({
+ fetched = network.post_json_api(domain, "/api/federation/instances", json.dumps({
"sort" : "+pubAt",
"host" : None,
"blocked": True,
from fba import config
from fba import fba
from fba import instances
+from fba import network
def fetch_peers(domain: str) -> list:
# DEBUG: print(f"DEBUG: domain({len(domain)})={domain},software='peertube' - CALLED!")
# DEBUG: print(f"DEBUG: domain='{domain}',mode='{mode}'")
while True:
try:
- response = fba.get_response(domain, "/api/v1/server/{mode}?start={start}&count=100", headers, (config.get("connection_timeout"), config.get("read_timeout")))
+ response = network.fetch_response(domain, "/api/v1/server/{mode}?start={start}&count=100", headers, (config.get("connection_timeout"), config.get("read_timeout")))
data = fba.json_from_response(response)
# DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code='{response.status_code}',data[]='{type(data)}'")
--- /dev/null
+# Fedi API Block - An aggregator for fetching blocking data from fediverse nodes
+# Copyright (C) 2023 Free Software Foundation
+#
+# 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 bs4
+import reqto
+import requests
+
+from fba import config
+from fba import instances
+
+def post_json_api(domain: str, path: str, parameter: str, extra_headers: dict = {}) -> dict:
+ # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',parameter='{parameter}',extra_headers()={len(extra_headers)} - CALLED!")
+ if type(domain) != str:
+ raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
+ elif domain == "":
+ raise ValueError(f"Parameter 'domain' is empty")
+ elif type(path) != str:
+ raise ValueError(f"path[]={type(path)} is not 'str'")
+ elif path == "":
+ raise ValueError("Parameter 'path' cannot be empty")
+ elif type(parameter) != str:
+ raise ValueError(f"parameter[]={type(parameter)} is not 'str'")
+
+ # DEBUG: print("DEBUG: Sending POST to domain,path,parameter:", domain, path, parameter, extra_headers)
+ data = {}
+ try:
+ response = reqto.post(
+ f"https://{domain}{path}",
+ data=parameter,
+ headers={**api_headers, **extra_headers},
+ timeout=(config.get("connection_timeout"), config.get("read_timeout"))
+ )
+
+ data = json_from_response(response)
+ # DEBUG: print(f"DEBUG: response.ok={response.ok},response.status_code={response.status_code},data[]='{type(data)}'")
+ if not response.ok or response.status_code >= 400:
+ print(f"WARNING: Cannot query JSON API: domain='{domain}',path='{path}',parameter()={len(parameter)},response.status_code='{response.status_code}',data[]='{type(data)}'")
+ instances.update_last_error(domain, response)
+
+ except BaseException as e:
+ print(f"WARNING: Some error during post(): domain='{domain}',path='{path}',parameter()={len(parameter)},exception[{type(e)}]:'{str(e)}'")
+
+ # DEBUG: print(f"DEBUG: Returning data({len(data)})=[]:{type(data)}")
+ return data
+
+def send_bot_post(instance: str, blocklist: dict):
+ # DEBUG: print(f"DEBUG: instance={instance},blocklist()={len(blocklist)} - CALLED!")
+ if type(domain) != str:
+ raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
+ elif domain == "":
+ raise ValueError("Parameter 'domain' is empty")
+ elif type(blocklist) != dict:
+ raise ValueError(f"Parameter blocklist[]='{type(blocklist)}' is not 'dict'")
+
+ message = instance + " has blocked the following instances:\n\n"
+ truncated = False
+
+ if len(blocklist) > 20:
+ truncated = True
+ blocklist = blocklist[0 : 19]
+
+ # DEBUG: print(f"DEBUG: blocklist()={len(blocklist)}")
+ for block in blocklist:
+ # DEBUG: print(f"DEBUG: block['{type(block)}']={block}")
+ if block["reason"] == None or block["reason"] == '':
+ message = message + block["blocked"] + " with unspecified reason\n"
+ else:
+ if len(block["reason"]) > 420:
+ block["reason"] = block["reason"][0:419] + "[…]"
+
+ message = message + block["blocked"] + ' for "' + block["reason"].replace("@", "@\u200b") + '"\n'
+
+ if truncated:
+ message = message + "(the list has been truncated to the first 20 entries)"
+
+ botheaders = {**api_headers, **{"Authorization": "Bearer " + config.get("bot_token")}}
+
+ req = reqto.post(
+ f"{config.get('bot_instance')}/api/v1/statuses",
+ data={
+ "status" : message,
+ "visibility" : config.get('bot_visibility'),
+ "content_type": "text/plain"
+ },
+ headers=botheaders,
+ timeout=10
+ ).json()
+
+ return True
+
+def fetch_friendica_blocks(domain: str) -> dict:
+ # DEBUG: print(f"DEBUG: domain='{domain}' - CALLED!")
+ if type(domain) != str:
+ raise ValueError(f"Parameter domain[]={type(domain)} is not 'str'")
+ elif domain == "":
+ raise ValueError(f"Parameter 'domain' is empty")
+
+ # DEBUG: print("DEBUG: Fetching friendica blocks from domain:", domain)
+ blocked = list()
+
+ try:
+ doc = bs4.BeautifulSoup(
+ fetch_response(domain, "/friendica", headers, (config.get("connection_timeout"), config.get("read_timeout"))).text,
+ "html.parser",
+ )
+ except BaseException as e:
+ print("WARNING: Failed to fetch /friendica from domain:", domain, e)
+ instances.update_last_error(domain, e)
+ return {}
+
+ blocklist = doc.find(id="about_blocklist")
+
+ # Prevents exceptions:
+ if blocklist is None:
+ # DEBUG: print("DEBUG: Instance has no block list:", domain)
+ return {}
+
+ table = blocklist.find("table")
+
+ # DEBUG: print(f"DEBUG: table[]='{type(table)}'")
+ if table.find("tbody"):
+ rows = table.find("tbody").find_all("tr")
+ else:
+ rows = table.find_all("tr")
+
+ # DEBUG: print(f"DEBUG: Found rows()={len(rows)}")
+ for line in rows:
+ # DEBUG: print(f"DEBUG: line='{line}'")
+ blocked.append({
+ "domain": tidyup_domain(line.find_all("td")[0].text),
+ "reason": tidyup_reason(line.find_all("td")[1].text)
+ })
+ # DEBUG: print("DEBUG: Next!")
+
+ # DEBUG: print("DEBUG: Returning blocklist() for domain:", domain, len(blocklist))
+ return {
+ "reject": blocked
+ }
+
+def fetch_response(domain: str, path: str, headers: dict, timeout: list) -> requests.models.Response:
+ # DEBUG: print(f"DEBUG: domain='{domain}',path='{path}',headers()={len(headers)},timeout={timeout} - CALLED!")
+ if type(domain) != str:
+ raise ValueError(f"Parameter domain[]='{type(domain)}' is not 'str'")
+ elif domain == "":
+ raise ValueError("Parameter 'domain' is empty")
+ elif type(path) != str:
+ raise ValueError(f"Parameter path[]='{type(path)}' is not 'str'")
+ elif path == "":
+ raise ValueError("Parameter 'path' is empty")
+
+ try:
+ # DEBUG: print(f"DEBUG: Sending request to '{domain}{path}' ...")
+ response = reqto.get(
+ f"https://{domain}{path}",
+ headers=headers,
+ timeout=timeout
+ );
+ except requests.exceptions.ConnectionError as e:
+ # DEBUG: print(f"DEBUG: Fetching '{path}' from '{domain}' failed. exception[{type(e)}]='{str(e)}'")
+ instances.update_last_error(domain, e)
+ raise e
+
+ # DEBUG: print(f"DEBUG: response[]='{type(response)}' - EXXIT!")
+ return response