2022-11-12 23:27:59 +00:00
""" UI pages. """
2022-11-11 19:12:48 +00:00
import datetime
2023-07-20 17:24:58 +00:00
import itertools
2022-11-20 19:56:32 +00:00
import logging
2023-02-07 21:51:15 +00:00
import os
2023-08-26 16:47:41 +00:00
import re
2023-10-13 16:46:30 +00:00
import time
2017-10-26 14:13:28 +00:00
2024-05-08 00:01:01 +00:00
from flask import render_template , request
2023-07-28 22:49:29 +00:00
from google . cloud . ndb import tasklets
2023-07-16 21:06:03 +00:00
from google . cloud . ndb . query import AND , OR
2022-11-09 15:53:00 +00:00
from google . cloud . ndb . stats import KindStat
2023-10-10 22:38:53 +00:00
from granary import as1 , as2 , atom , microformats2 , rss
2022-12-02 22:46:18 +00:00
import humanize
2021-09-09 05:14:11 +00:00
from oauth_dropins . webutil import flask_util , logs , util
2023-10-16 21:02:17 +00:00
from oauth_dropins . webutil . flask_util import (
canonicalize_request_domain ,
error ,
flash ,
redirect ,
)
2024-04-23 03:21:56 +00:00
import requests
import werkzeug . exceptions
2017-10-26 04:32:59 +00:00
2024-05-05 20:02:44 +00:00
from activitypub import ActivityPub , instance_actor
2022-11-08 14:56:19 +00:00
import common
2023-03-08 21:10:41 +00:00
from common import DOMAIN_RE
2023-05-31 00:24:49 +00:00
from flask_app import app , cache
2024-04-12 03:02:53 +00:00
import ids
2023-11-23 04:39:21 +00:00
from models import fetch_objects , fetch_page , Follower , Object , PAGE_SIZE , PROTOCOLS
2023-09-28 21:42:18 +00:00
from protocol import Protocol
2022-11-11 23:44:35 +00:00
2023-02-14 20:52:14 +00:00
# precompute this because we get a ton of requests for non-existing users
# from weird open redirect referrers:
# https://github.com/snarfed/bridgy-fed/issues/422
with app . test_request_context ( ' / ' ) :
USER_NOT_FOUND_HTML = render_template ( ' user_not_found.html ' )
2022-11-20 19:56:32 +00:00
logger = logging . getLogger ( __name__ )
2023-10-11 04:19:26 +00:00
TEMPLATE_VARS = {
2024-04-02 06:06:25 +00:00
' as1 ' : as1 ,
2023-10-11 04:19:26 +00:00
' as2 ' : as2 ,
2024-04-12 03:02:53 +00:00
' ids ' : ids ,
2023-10-11 04:19:26 +00:00
' isinstance ' : isinstance ,
' logs ' : logs ,
' PROTOCOLS ' : PROTOCOLS ,
' set ' : set ,
' util ' : util ,
}
2017-10-26 04:32:59 +00:00
2023-06-07 18:51:31 +00:00
def load_user ( protocol , id ) :
2023-11-20 04:48:31 +00:00
""" Loads and returns the current request ' s user.
2023-06-07 18:51:31 +00:00
Args :
2023-10-06 06:32:31 +00:00
protocol ( str ) :
id ( str ) :
2023-06-07 18:51:31 +00:00
2023-11-20 04:39:05 +00:00
Returns :
models . User :
2023-06-07 18:51:31 +00:00
Raises :
: class : ` werkzeug . exceptions . HTTPException ` on error or redirect
"""
2023-06-10 22:07:26 +00:00
assert id
2023-10-10 21:55:27 +00:00
2023-06-07 18:51:31 +00:00
cls = PROTOCOLS [ protocol ]
2024-04-11 21:24:18 +00:00
if cls . ABBREV == ' ap ' and not id . startswith ( ' @ ' ) :
id = ' @ ' + id
2023-11-20 04:48:31 +00:00
user = cls . get_by_id ( id )
2023-06-07 18:51:31 +00:00
2024-04-11 21:24:18 +00:00
if cls . ABBREV != ' web ' :
2023-11-20 04:48:31 +00:00
if not user :
2024-04-27 16:53:21 +00:00
user = cls . query ( cls . handle == id ) . get ( )
2023-11-20 04:48:31 +00:00
if user and user . use_instead :
user = user . use_instead . get ( )
2023-06-07 18:51:31 +00:00
2023-11-20 04:48:31 +00:00
if user and id not in ( user . key . id ( ) , user . handle ) :
error ( ' ' , status = 302 , location = user . user_page_path ( ) )
2023-06-07 18:51:31 +00:00
2023-11-20 04:48:31 +00:00
elif user and id != user . key . id ( ) : # use_instead redirect
error ( ' ' , status = 302 , location = user . user_page_path ( ) )
2023-06-07 18:51:31 +00:00
2024-04-27 16:53:21 +00:00
if user and ( user . direct or user . enabled_protocols or cls . ABBREV == ' web ' ) :
2024-01-22 21:12:20 +00:00
assert not user . use_instead
return user
# TODO: switch back to USER_NOT_FOUND_HTML
# not easy via exception/abort because this uses Werkzeug's built in
# NotFound exception subclass, and we'd need to make it implement
# get_body to return arbitrary HTML.
error ( f ' { protocol } user { id } not found ' , status = 404 )
2023-06-07 18:51:31 +00:00
2022-11-11 19:12:48 +00:00
@app.route ( ' / ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2022-11-11 19:12:48 +00:00
@flask_util.cached ( cache , datetime . timedelta ( days = 1 ) )
def front_page ( ) :
2022-11-19 06:30:07 +00:00
""" View for the front page. """
return render_template ( ' index.html ' )
@app.route ( ' /docs ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2022-11-19 06:30:07 +00:00
@flask_util.cached ( cache , datetime . timedelta ( days = 1 ) )
def docs ( ) :
""" View for the docs page. """
return render_template ( ' docs.html ' )
2023-01-19 06:20:15 +00:00
@app.get ( f ' /user/<regex( " { DOMAIN_RE } " ):domain> ' )
2023-05-30 21:08:13 +00:00
@app.get ( f ' /user/<regex( " { DOMAIN_RE } " ):domain>/feed ' )
@app.get ( f ' /user/<regex( " { DOMAIN_RE } " ):domain>/<any(followers,following):collection> ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-05-30 21:08:13 +00:00
def web_user_redirects ( * * kwargs ) :
path = request . url . removeprefix ( request . root_url ) . removeprefix ( ' user/ ' )
return redirect ( f ' /web/ { path } ' , code = 301 )
2023-06-02 05:00:47 +00:00
@app.get ( f ' /<any( { " , " . join ( PROTOCOLS ) } ):protocol>/<id> ' )
2023-09-26 23:43:48 +00:00
# WARNING: this overrides the /ap/... actor URL route in activitypub.py, *only*
# for handles with leading @ character. be careful when changing this route!
@app.get ( f ' /ap/@<id> ' , defaults = { ' protocol ' : ' ap ' } )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-10-10 21:55:27 +00:00
def profile ( protocol , id ) :
2023-11-20 04:48:31 +00:00
user = load_user ( protocol , id )
query = Object . query ( Object . users == user . key )
2023-11-20 23:36:26 +00:00
objects , before , after = fetch_objects ( query , by = Object . updated , user = user )
2023-11-20 04:48:31 +00:00
num_followers , num_following = user . count_followers ( )
2023-10-11 04:19:26 +00:00
return render_template ( ' profile.html ' , * * TEMPLATE_VARS , * * locals ( ) )
2023-10-10 21:55:27 +00:00
@app.get ( f ' /<any( { " , " . join ( PROTOCOLS ) } ):protocol>/<id>/home ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-10-10 21:55:27 +00:00
def home ( protocol , id ) :
2023-11-20 04:48:31 +00:00
user = load_user ( protocol , id )
query = Object . query ( Object . feed == user . key )
2023-11-20 23:36:26 +00:00
objects , before , after = fetch_objects ( query , by = Object . created , user = user )
2023-10-11 19:22:34 +00:00
# this calls Object.actor_link serially for each object, which loads the
# actor from the datastore if necessary. TODO: parallelize those fetches
2023-10-11 04:19:26 +00:00
return render_template ( ' home.html ' , * * TEMPLATE_VARS , * * locals ( ) )
2023-10-10 21:55:27 +00:00
@app.get ( f ' /<any( { " , " . join ( PROTOCOLS ) } ):protocol>/<id>/notifications ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-10-10 21:55:27 +00:00
def notifications ( protocol , id ) :
2023-11-20 04:48:31 +00:00
user = load_user ( protocol , id )
2023-10-10 21:55:27 +00:00
2023-11-20 04:48:31 +00:00
query = Object . query ( Object . notify == user . key )
2023-11-20 23:36:26 +00:00
objects , before , after = fetch_objects ( query , by = Object . updated , user = user )
2023-10-11 18:28:39 +00:00
format = request . args . get ( ' format ' )
if format :
2023-10-12 17:37:44 +00:00
return serve_feed ( objects = objects , format = format , as_snippets = True ,
2023-11-20 04:48:31 +00:00
user = user , title = f ' Bridgy Fed notifications for { id } ' ,
2023-10-12 17:48:29 +00:00
quiet = request . args . get ( ' quiet ' ) )
2023-10-11 18:28:39 +00:00
# notifications tab UI page
2023-10-11 04:19:26 +00:00
return render_template ( ' notifications.html ' , * * TEMPLATE_VARS , * * locals ( ) )
2022-11-08 14:56:19 +00:00
2022-11-11 23:44:35 +00:00
2024-04-23 03:21:56 +00:00
@app.post ( f ' /<any( { " , " . join ( PROTOCOLS ) } ):protocol>/<id>/update-profile ' )
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
def update_profile ( protocol , id ) :
user = load_user ( protocol , id )
2024-04-28 13:50:46 +00:00
link = f ' <a href= " { user . web_url ( ) } " > { user . handle_or_id ( ) } </a> '
2024-04-23 03:21:56 +00:00
try :
profile_obj = user . load ( user . profile_id ( ) , remote = True )
except ( requests . RequestException , werkzeug . exceptions . HTTPException ) as e :
2024-04-25 00:06:25 +00:00
_ , msg = util . interpret_http_exception ( e )
2024-04-28 13:50:46 +00:00
flash ( f " Couldn ' t update profile for { link } : { msg } " )
2024-05-06 02:22:02 +00:00
return redirect ( user . user_page_path ( ) , code = 302 )
2024-04-23 03:21:56 +00:00
2024-04-25 00:06:25 +00:00
if profile_obj :
common . create_task ( queue = ' receive ' , obj = profile_obj . key . urlsafe ( ) ,
authed_as = id )
2024-04-28 13:50:46 +00:00
flash ( f ' Updating profile from { link } ... ' )
2024-04-23 03:21:56 +00:00
else :
2024-04-28 13:50:46 +00:00
flash ( f " Couldn ' t update profile for { link } " )
2024-04-23 03:21:56 +00:00
return redirect ( user . user_page_path ( ) , code = 302 )
2023-06-02 05:00:47 +00:00
@app.get ( f ' /<any( { " , " . join ( PROTOCOLS ) } ):protocol>/<id>/<any(followers,following):collection> ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-06-02 05:00:47 +00:00
def followers_or_following ( protocol , id , collection ) :
2023-11-20 04:39:05 +00:00
user = load_user ( protocol , id )
followers , before , after = Follower . fetch_page ( collection , user )
2023-11-20 04:48:31 +00:00
num_followers , num_following = user . count_followers ( )
2022-11-12 16:25:36 +00:00
return render_template (
2023-01-19 05:22:04 +00:00
f ' { collection } .html ' ,
2023-02-09 16:23:31 +00:00
address = request . args . get ( ' address ' ) ,
2023-10-10 21:55:27 +00:00
follow_url = request . values . get ( ' url ' ) ,
2023-10-11 04:19:26 +00:00
* * TEMPLATE_VARS ,
2023-10-10 21:55:27 +00:00
* * locals ( ) ,
2022-11-12 16:25:36 +00:00
)
2023-06-02 05:00:47 +00:00
@app.get ( f ' /<any( { " , " . join ( PROTOCOLS ) } ):protocol>/<id>/feed ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-06-02 05:00:47 +00:00
def feed ( protocol , id ) :
2023-11-20 04:48:31 +00:00
user = load_user ( protocol , id )
query = Object . query ( Object . feed == user . key )
2023-11-20 23:36:26 +00:00
objects , _ , _ = fetch_objects ( query , by = Object . created , user = user )
2023-10-11 18:28:39 +00:00
return serve_feed ( objects = objects , format = request . args . get ( ' format ' , ' html ' ) ,
2023-11-20 04:48:31 +00:00
user = user , title = f ' Bridgy Fed feed for { id } ' )
2023-10-11 18:28:39 +00:00
2023-11-20 04:48:31 +00:00
def serve_feed ( * , objects , format , user , title , as_snippets = False , quiet = False ) :
2023-10-11 18:28:39 +00:00
""" Generates a feed based on :class:`Object`s.
Args :
objects ( sequence of models . Object )
format ( str ) : ` ` html ` ` , ` ` atom ` ` , or ` ` rss ` `
2023-11-20 04:48:31 +00:00
user ( models . User )
2023-10-11 18:28:39 +00:00
title ( str )
2023-10-12 17:37:44 +00:00
as_snippets ( bool ) : if True , render short snippets for objects instead of
full contents
2023-10-12 17:48:29 +00:00
quiet ( bool ) : if True , exclude follows , unfollows , likes , and reposts
2023-10-11 18:28:39 +00:00
Returns :
str or ( str , dict ) tuple : Flask response
"""
if format not in ( ' html ' , ' atom ' , ' rss ' ) :
error ( f ' format { format } not supported; expected html, atom, or rss ' )
2023-10-12 17:48:29 +00:00
objects = [ obj for obj in objects if not obj . deleted ]
if quiet :
objects = [ obj for obj in objects if obj . type not in
2023-12-27 04:42:11 +00:00
( ' delete ' , ' follow ' , ' stop-following ' , ' like ' , ' share ' ,
' undo ' , ' update ' ) ]
2023-10-12 17:48:29 +00:00
2023-10-12 17:37:44 +00:00
if as_snippets :
activities = [ {
' objectType ' : ' note ' ,
2023-10-13 13:41:08 +00:00
' id ' : obj . key . id ( ) ,
2023-11-26 04:38:28 +00:00
' content ' : f ' { obj . actor_link ( image = False , user = user ) } { obj . phrase } { obj . content } ' ,
2023-10-12 17:37:44 +00:00
' content_is_html ' : True ,
' updated ' : obj . updated . isoformat ( ) ,
2023-10-17 17:09:32 +00:00
' url ' : as1 . get_url ( obj . as1 ) or as1 . get_url ( as1 . get_object ( obj . as1 ) ) ,
2023-10-12 17:48:29 +00:00
} for obj in objects ]
2023-10-12 17:37:44 +00:00
else :
2023-10-12 17:48:29 +00:00
activities = [ obj . as1 for obj in objects ]
2023-07-28 22:49:29 +00:00
# hydrate authors, actors, objects from stored Objects
2023-07-20 17:24:58 +00:00
fields = ' author ' , ' actor ' , ' object '
2023-07-28 22:49:29 +00:00
gets = [ ]
for a in activities :
for field in fields :
val = as1 . get_object ( a , field )
if val and val . keys ( ) < = set ( [ ' id ' ] ) :
def hydrate ( a , f ) :
def maybe_set ( future ) :
if future . result ( ) and future . result ( ) . as1 :
a [ f ] = future . result ( ) . as1
return maybe_set
2024-04-12 19:04:52 +00:00
# TODO: extract a Protocol class method out of User.profile_id,
# then use that here instead. the catch is that we'd need to
# determine Protocol for every id, which is expensive.
2024-04-12 19:34:49 +00:00
#
# same TODO is in models.fetch_objects
2024-04-12 19:04:52 +00:00
id = val [ ' id ' ]
if id . startswith ( ' did: ' ) :
id = f ' at:// { id } /app.bsky.actor.profile/self '
future = Object . get_by_id_async ( id )
2023-07-28 22:49:29 +00:00
future . add_done_callback ( hydrate ( a , field ) )
gets . append ( future )
tasklets . wait_all ( gets )
2023-07-20 05:39:22 +00:00
2023-11-20 04:48:31 +00:00
actor = ( user . obj . as1 if user . obj and user . obj . as1
else { ' displayName ' : user . readable_id , ' url ' : user . web_url ( ) } )
2023-10-23 20:10:27 +00:00
2023-03-05 15:52:56 +00:00
# TODO: inject/merge common.pretty_link into microformats2.render_content
# (specifically into hcard_to_html) somehow to convert Mastodon URLs to @-@
# syntax. maybe a fediverse kwarg down through the call chain?
2022-11-17 15:38:52 +00:00
if format == ' html ' :
2023-01-28 23:07:05 +00:00
entries = [ microformats2 . object_to_html ( a ) for a in activities ]
2023-10-11 04:19:26 +00:00
return render_template ( ' feed.html ' , * * TEMPLATE_VARS , * * locals ( ) )
2022-11-17 15:38:52 +00:00
elif format == ' atom ' :
2023-10-23 20:10:27 +00:00
body = atom . activities_to_atom ( activities , actor = actor , title = title ,
request_url = request . url )
2022-11-17 15:58:08 +00:00
return body , { ' Content-Type ' : atom . CONTENT_TYPE }
2022-11-17 15:38:52 +00:00
elif format == ' rss ' :
2023-10-23 20:10:27 +00:00
body = rss . from_activities ( activities , actor = actor , title = title ,
feed_url = request . url )
2022-11-17 15:58:08 +00:00
return body , { ' Content-Type ' : rss . CONTENT_TYPE }
2022-11-17 15:38:52 +00:00
2024-03-13 20:19:34 +00:00
# TODO: re-enable for launch
# @app.get('/bridge-user')
# @canonicalize_request_domain(common.PROTOCOL_DOMAINS, common.PRIMARY_DOMAIN)
# @flask_util.cached(cache, datetime.timedelta(days=1))
# def bridge_user_page():
# return render_template('bridge_user.html')
# @app.post('/bridge-user')
# def bridge_user():
# handle = request.values['handle']
# proto, id = Protocol.for_handle(handle)
# if not proto:
# flash(f"Couldn't determine protocol for {handle}")
# return render_template('bridge_user.html'), 400
# # TODO: put these into a PULL_PROTOCOLS constant?
# if not proto.LABEL in ('activitypub', 'fake', 'web'):
# flash(f"{proto.__name__} isn't supported")
# return render_template('bridge_user.html'), 400
# if not id:
# id = proto.handle_to_id(handle)
# if not id:
# flash(f"Couldn't resolve {proto.__name__} handle {handle}")
# return render_template('bridge_user.html'), 400
# user = proto.get_or_create(id=id, propagate=True)
# flash(f'Bridging <a href="{user.web_url()}">{user.handle}</a> into Bluesky. <a href="https://bsky.app/search">Try searching for them</a> in a minute!')
# return render_template('bridge_user.html')
2023-09-28 21:42:18 +00:00
2022-11-09 15:53:00 +00:00
@app.get ( ' /stats ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2022-11-09 15:53:00 +00:00
def stats ( ) :
2022-12-02 22:46:18 +00:00
def count ( kind ) :
return humanize . intcomma (
KindStat . query ( KindStat . kind_name == kind ) . get ( ) . count )
return render_template (
' stats.html ' ,
users = count ( ' MagicKey ' ) ,
2023-01-28 23:07:05 +00:00
objects = count ( ' Object ' ) ,
2022-12-02 22:46:18 +00:00
followers = count ( ' Follower ' ) ,
)
2022-11-09 15:53:00 +00:00
2023-02-07 21:51:15 +00:00
@app.get ( ' /.well-known/nodeinfo ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-02-07 21:51:15 +00:00
@flask_util.cached ( cache , datetime . timedelta ( days = 1 ) )
def nodeinfo_jrd ( ) :
"""
https : / / nodeinfo . diaspora . software / protocol . html
"""
return {
' links ' : [ {
' rel ' : ' http://nodeinfo.diaspora.software/ns/schema/2.1 ' ,
' href ' : common . host_url ( ' nodeinfo.json ' ) ,
2024-05-05 20:02:44 +00:00
} , {
" rel " : " https://www.w3.org/ns/activitystreams#Application " ,
" href " : instance_actor ( ) . id_as ( ActivityPub ) ,
2023-02-07 21:51:15 +00:00
} ] ,
} , {
' Content-Type ' : ' application/jrd+json ' ,
}
@app.get ( ' /nodeinfo.json ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2023-02-07 21:51:15 +00:00
@flask_util.cached ( cache , datetime . timedelta ( days = 1 ) )
def nodeinfo ( ) :
"""
https : / / nodeinfo . diaspora . software / schema . html
"""
2023-05-26 23:36:45 +00:00
user_total = None
stat = KindStat . query ( KindStat . kind_name == ' MagicKey ' ) . get ( )
if stat :
user_total = stat . count
2023-02-07 21:51:15 +00:00
return {
' version ' : ' 2.1 ' ,
' software ' : {
' name ' : ' bridgy-fed ' ,
' version ' : os . getenv ( ' GAE_VERSION ' ) ,
' repository ' : ' https://github.com/snarfed/bridgy-fed ' ,
2023-06-01 01:34:33 +00:00
' web_url ' : ' https://fed.brid.gy/ ' ,
2023-02-07 21:51:15 +00:00
} ,
' protocols ' : [
' activitypub ' ,
' bluesky ' ,
' webmention ' ,
] ,
' services ' : {
' outbound ' : [ ] ,
' inbound ' : [ ] ,
} ,
' usage ' : {
' users ' : {
2023-05-26 23:36:45 +00:00
' total ' : user_total ,
2023-02-07 21:51:15 +00:00
# 'activeMonth':
# 'activeHalfyear':
} ,
2023-12-31 17:04:17 +00:00
# these are too heavy
# 'localPosts': Object.query(Object.source_protocol.IN(('web', 'webmention')),
# Object.type.IN(['note', 'article']),
# ).count(),
# 'localComments': Object.query(Object.source_protocol.IN(('web', 'webmention')),
# Object.type == 'comment',
# ).count(),
2023-02-07 21:51:15 +00:00
} ,
' openRegistrations ' : True ,
' metadata ' : { } ,
} , {
# https://nodeinfo.diaspora.software/protocol.html
' Content-Type ' : ' application/json; profile= " http://nodeinfo.diaspora.software/ns/schema/2.1# " ' ,
}
2023-12-31 16:42:26 +00:00
@app.get ( ' /api/v1/instance ' )
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
@flask_util.cached ( cache , datetime . timedelta ( days = 1 ) )
def instance_info ( ) :
"""
https : / / docs . joinmastodon . org / methods / instance / #v1
"""
return {
' uri ' : ' fed.brid.gy ' ,
' title ' : ' Bridgy Fed ' ,
' version ' : os . getenv ( ' GAE_VERSION ' ) ,
' short_description ' : ' Bridging the new social internet ' ,
' description ' : ' Bridging the new social internet ' ,
2024-01-17 16:55:33 +00:00
' email ' : ' feedback@brid.gy ' ,
2023-12-31 16:42:26 +00:00
' thumbnail ' : ' https://fed.brid.gy/static/bridgy_logo_with_alpha.png ' ,
' registrations ' : True ,
' approval_required ' : False ,
' invites_enabled ' : False ,
' contact_account ' : {
' username ' : ' snarfed.org ' ,
' acct ' : ' snarfed.org ' ,
' display_name ' : ' Ryan ' ,
' url ' : ' https://snarfed.org/ ' ,
} ,
}
2021-07-13 15:06:35 +00:00
@app.get ( ' /log ' )
2023-10-16 21:02:17 +00:00
@canonicalize_request_domain ( common . PROTOCOL_DOMAINS , common . PRIMARY_DOMAIN )
2021-09-09 05:14:11 +00:00
@flask_util.cached ( cache , logs . CACHE_TIME )
2021-07-13 15:06:35 +00:00
def log ( ) :
2021-08-06 17:30:50 +00:00
return logs . log ( )