Search for address

master
Bob Mottram 2019-07-30 23:34:04 +01:00
parent ce0bd875f5
commit bcd6278e82
4 changed files with 232 additions and 19 deletions

View File

@ -66,7 +66,9 @@ from webinterface import htmlLogin
from webinterface import htmlGetLoginCredentials from webinterface import htmlGetLoginCredentials
from webinterface import htmlNewPost from webinterface import htmlNewPost
from webinterface import htmlFollowConfirm from webinterface import htmlFollowConfirm
from webinterface import htmlSearch
from webinterface import htmlUnfollowConfirm from webinterface import htmlUnfollowConfirm
from webinterface import htmlProfileAfterSearch
from shares import getSharesFeedForPerson from shares import getSharesFeedForPerson
from shares import outboxShareUpload from shares import outboxShareUpload
from shares import outboxUndoShareUpload from shares import outboxUndoShareUpload
@ -611,7 +613,16 @@ class PubServer(BaseHTTPRequestHandler):
self._redirect_headers(originPathStr,cookie) self._redirect_headers(originPathStr,cookie)
self.server.GETbusy=False self.server.GETbusy=False
return return
# search for a fediverse address from the web interface by selecting search icon
if '/users/' in self.path:
if self.path.endswith('/search'):
# show the search screen
self._set_headers('text/html',cookie)
self.wfile.write(htmlSearch(self.server.baseDir,self.path).encode())
self.server.GETbusy=False
return
# Unfollow a person from the web interface by selecting Unfollow on the dropdown # Unfollow a person from the web interface by selecting Unfollow on the dropdown
if '/users/' in self.path: if '/users/' in self.path:
if '?unfollow=' in self.path: if '?unfollow=' in self.path:
@ -1459,6 +1470,42 @@ class PubServer(BaseHTTPRequestHandler):
self._redirect_headers(originPathStr,cookie) self._redirect_headers(originPathStr,cookie)
self.server.POSTbusy=False self.server.POSTbusy=False
# decision to follow in the web interface is confirmed
if authorized and self.path.endswith('/searchhandle'):
actorStr=self.path.replace('/searchhandle','')
length = int(self.headers['Content-length'])
searchParams=self.rfile.read(length).decode('utf-8')
if 'searchtext=' in searchParams:
searchStr=searchParams.split('searchtext=')[1]
if '&' in searchStr:
searchStr=searchStr.split('&')[0]
searchStr=searchStr.replace('+',' ').replace('%40','@')
if '@' in searchStr:
print('Search: '+searchStr)
nickname=getNicknameFromActor(self.path)
if not self.server.session:
self.server.session= \
createSession(self.server.domain,self.server.port,self.server.useTor)
profileStr= \
htmlProfileAfterSearch(self.server.baseDir, \
self.path.replace('/searchhandle',''), \
self.server.httpPrefix, \
nickname, \
self.server.domain,self.server.port, \
searchStr, \
self.server.session, \
self.server.cachedWebfingers, \
self.server.personCache, \
self.server.debug)
if profileStr:
self._login_headers('text/html')
self.wfile.write(profileStr.encode('utf-8'))
self.server.POSTbusy=False
return
self._redirect_headers(actorStr,cookie)
self.server.POSTbusy=False
return
# decision to follow in the web interface is confirmed # decision to follow in the web interface is confirmed
if authorized and self.path.endswith('/followconfirm'): if authorized and self.path.endswith('/followconfirm'):
originPathStr=self.path.split('/followconfirm')[0] originPathStr=self.path.split('/followconfirm')[0]

View File

@ -49,3 +49,12 @@ body, html {
background-color: #555; background-color: #555;
color: white; color: white;
} }
input[type=text] {
width: 70%;
clear: both;
font-size: 24px;
text-align: center;
color: var(--text-entry-foreground);
background-color: var(--text-entry-background);
}

View File

@ -209,6 +209,7 @@ body, html {
float: left; float: left;
max-width: 60px; max-width: 60px;
width: 10%; width: 10%;
padding: 0px 7px;
margin-right: 20px; margin-right: 20px;
border-radius: 10%; border-radius: 10%;
} }
@ -216,22 +217,23 @@ body, html {
.containericons { .containericons {
border: 0px solid #dedede; border: 0px solid #dedede;
background-color: #f1f1f1; background-color: #f1f1f1;
border-radius: 5px; border-radius: 0px;
padding: 10px; padding: 0px;
margin: 10px 0; margin: 0px 0;
}
.darker {
border-color: #ccc;
background-color: #ddd;
} }
.containericons img { .containericons img {
float: right; float: right;
max-width: 35px; max-width: 35px;
width: 5%; width: 5%;
margin-right: 50px; margin-right: 10px;
border-radius: 10%; padding-left: 0px 0px;
border-radius: 0%;
}
.darker {
border-color: #ccc;
background-color: #ddd;
} }
.container img.attachment { .container img.attachment {

View File

@ -16,6 +16,10 @@ from utils import getNicknameFromActor
from utils import getDomainFromActor from utils import getDomainFromActor
from posts import getPersonBox from posts import getPersonBox
from follow import isFollowingActor from follow import isFollowingActor
from webfinger import webfingerHandle
from posts import getUserUrl
from posts import parseUserFeed
from session import getJson
def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str): def htmlGetLoginCredentials(loginParams: str,lastLoginTime: int) -> (str,str):
"""Receives login credentials via HTTPServer POST """Receives login credentials via HTTPServer POST
@ -198,7 +202,7 @@ def htmlProfilePosts(baseDir: str,httpPrefix: str, \
if item['type']=='Create': if item['type']=='Create':
profileStr+= \ profileStr+= \
individualPostAsHtml(baseDir,session,wfRequest,personCache, \ individualPostAsHtml(baseDir,session,wfRequest,personCache, \
nickname,domain,port,item,False) nickname,domain,port,item,None,True,False)
return profileStr return profileStr
def htmlProfileFollowing(baseDir: str,httpPrefix: str, \ def htmlProfileFollowing(baseDir: str,httpPrefix: str, \
@ -413,6 +417,7 @@ def individualPostAsHtml(baseDir: str, \
session,wfRequest: {},personCache: {}, \ session,wfRequest: {},personCache: {}, \
nickname: str,domain: str,port: int, \ nickname: str,domain: str,port: int, \
postJsonObject: {}, \ postJsonObject: {}, \
avatarUrl: str, showAvatarDropdown: bool,
showIcons=False) -> str: showIcons=False) -> str:
avatarPosition='' avatarPosition=''
containerClass='container' containerClass='container'
@ -457,7 +462,8 @@ def individualPostAsHtml(baseDir: str, \
'<img src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment"></a>\n' '<img src="'+attach['url']+'" alt="'+imageDescription+'" title="'+imageDescription+'" class="attachment"></a>\n'
attachmentCtr+=1 attachmentCtr+=1
avatarUrl=postJsonObject['actor']+'/avatar.png' if not avatarUrl:
avatarUrl=postJsonObject['actor']+'/avatar.png'
fullDomain=domain fullDomain=domain
if port!=80 and port!=443: if port!=80 and port!=443:
@ -484,7 +490,7 @@ def individualPostAsHtml(baseDir: str, \
avatarDropdown= \ avatarDropdown= \
' <div class="dropdown-timeline">' \ ' <div class="dropdown-timeline">' \
' <img src="'+avatarUrl+'" alt="Avatar"'+avatarPosition+'/>' \ ' <img src="'+avatarUrl+'" '+avatarPosition+'/>' \
' <div class="dropdown-timeline-content">' \ ' <div class="dropdown-timeline-content">' \
' <a href="'+postJsonObject['actor']+'">Visit</a>'+ \ ' <a href="'+postJsonObject['actor']+'">Visit</a>'+ \
followUnfollowStr+ \ followUnfollowStr+ \
@ -496,9 +502,12 @@ def individualPostAsHtml(baseDir: str, \
footerStr='<span class="'+timeClass+'">'+postJsonObject['object']['published']+'</span>\n' footerStr='<span class="'+timeClass+'">'+postJsonObject['object']['published']+'</span>\n'
if showIcons: if showIcons:
footerStr='<div class="'+containerClassIcons+'">' footerStr='<div class="'+containerClassIcons+'">'
footerStr+='<img src="/icons/reply.png"/>' footerStr+='<a href="/users/'+nickname+'?replyto='+postJsonObject['object']['id']+'">'
footerStr+='<img src="/icons/repeat_inactive.png"/>' footerStr+='<img src="/icons/reply.png"/></a>'
footerStr+='<img src="/icons/like_inactive.png"/>' footerStr+='<a href="/users/'+nickname+'?repeat='+postJsonObject['object']['id']+'">'
footerStr+='<img src="/icons/repeat_inactive.png"/></a>'
footerStr+='<a href="/users/'+nickname+'?like='+postJsonObject['object']['id']+'">'
footerStr+='<img src="/icons/like_inactive.png"/></a>'
footerStr+='<span class="'+timeClass+'">'+postJsonObject['object']['published']+'</span>' footerStr+='<span class="'+timeClass+'">'+postJsonObject['object']['published']+'</span>'
footerStr+='</div>' footerStr+='</div>'
@ -557,7 +566,7 @@ def htmlTimeline(session,baseDir: str,wfRequest: {},personCache: {}, \
for item in timelineJson['orderedItems']: for item in timelineJson['orderedItems']:
if item['type']=='Create': if item['type']=='Create':
tlStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \ tlStr+=individualPostAsHtml(baseDir,session,wfRequest,personCache, \
nickname,domain,port,item,showIndividualPostIcons) nickname,domain,port,item,None,True,showIndividualPostIcons)
tlStr+=htmlFooter() tlStr+=htmlFooter()
return tlStr return tlStr
@ -581,7 +590,7 @@ def htmlIndividualPost(baseDir: str,session,wfRequest: {},personCache: {}, \
""" """
return htmlHeader()+ \ return htmlHeader()+ \
individualPostAsHtml(baseDir,session,wfRequest,personCache, \ individualPostAsHtml(baseDir,session,wfRequest,personCache, \
nickname,domain,port,postJsonObject,False)+ \ nickname,domain,port,postJsonObject,None,True,False)+ \
htmlFooter() htmlFooter()
def htmlPostReplies(postJsonObject: {}) -> str: def htmlPostReplies(postJsonObject: {}) -> str:
@ -648,3 +657,149 @@ def htmlUnfollowConfirm(baseDir: str,originPathStr: str,followActor: str,followP
followStr+='</div>' followStr+='</div>'
followStr+=htmlFooter() followStr+=htmlFooter()
return followStr return followStr
def htmlSearch(baseDir: str,path: str) -> str:
"""Search called from the timeline icon
"""
actor=path.replace('/search','')
nickname=getNicknameFromActor(actor)
domain,port=getDomainFromActor(actor)
if os.path.isfile(baseDir+'/img/search-background.png'):
if not os.path.isfile(baseDir+'/accounts/search-background.png'):
copyfile(baseDir+'/img/search-background.png',baseDir+'/accounts/search-background.png')
with open(baseDir+'/epicyon-follow.css', 'r') as cssFile:
profileStyle = cssFile.read()
followStr=htmlHeader(profileStyle)
followStr+='<div class="follow">'
followStr+=' <div class="followAvatar">'
followStr+=' <center>'
followStr+=' <p class="followText">Enter an address to search for</p>'
followStr+= \
' <form method="POST" action="'+actor+'/searchhandle">' \
' <input type="hidden" name="actor" value="'+actor+'">' \
' <input type="text" name="searchtext" autofocus><br>' \
' <button type="submit" class="button" name="submitSearch">Submit</button>' \
' <a href="'+actor+'"><button class="button">Go Back</button></a>' \
' </form>'
followStr+=' </center>'
followStr+=' </div>'
followStr+='</div>'
followStr+=htmlFooter()
return followStr
def htmlProfileAfterSearch(baseDir: str,path: str,httpPrefix: str, \
nickname: str,domain: str,port: int, \
profileHandle: str, \
session,wfRequest: {},personCache: {},
debug: bool) -> str:
"""Show a profile page after a search for a fediverse address
"""
if '/users/' in profileHandle:
searchNickname=getNicknameFromActor(profileHandle)
searchDomain,searchPort=getDomainFromActor(profileHandle)
else:
if '@' not in profileHandle:
if debug:
print('DEBUG: no @ in '+profileHandle)
return None
if profileHandle.startswith('@'):
profileHandle=profileHandle[1:]
if '@' not in profileHandle:
if debug:
print('DEBUG: no @ in '+profileHandle)
return None
searchNickname=profileHandle.split('@')[0]
searchDomain=profileHandle.split('@')[1]
searchPort=None
if ':' in searchDomain:
searchPort=int(searchDomain.split(':')[1])
searchDomain=searchDomain.split(':')[0]
if not searchNickname:
if debug:
print('DEBUG: No nickname found in '+profileHandle)
return None
if not searchDomain:
if debug:
print('DEBUG: No domain found in '+profileHandle)
return None
searchDomainFull=searchDomain
if searchPort:
if searchPort!=80 and searchPort!=443:
searchDomainFull=searchDomain+':'+str(searchPort)
profileStr=''
with open(baseDir+'/epicyon-profile.css', 'r') as cssFile:
wf = webfingerHandle(session,searchNickname+'@'+searchDomain,httpPrefix,wfRequest)
if not wf:
if debug:
print('DEBUG: Unable to webfinger '+searchNickname+'@'+searchDomain)
return None
asHeader = {'Accept': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"'}
personUrl = getUserUrl(wf)
profileJson = getJson(session,personUrl,asHeader,None)
if not profileJson:
if debug:
print('DEBUG: No actor returned from '+personUrl)
return None
avatarUrl=''
if profileJson.get('icon'):
if profileJson['icon'].get('url'):
avatarUrl=profileJson['icon']['url']
preferredName=searchNickname
if profileJson.get('preferredUsername'):
preferredName=profileJson['preferredUsername']
profileDescription=''
if profileJson.get('publicKey'):
if profileJson['publicKey'].get('summary'):
profileDescription=profileJson['publicKey']['summary']
outboxUrl=None
if not profileJson.get('outbox'):
if debug:
pprint(profileJson)
print('DEBUG: No outbox found')
return None
outboxUrl=profileJson['outbox']
profileBackgroundImage=''
if profileJson.get('image'):
if profileJson['image'].get('url'):
profileBackgroundImage=profileJson['image']['url']
profileStyle = cssFile.read().replace('image.png',profileBackgroundImage)
profileStr= \
' <div class="hero-image">' \
' <div class="hero-text">' \
' <img src="'+avatarUrl+'" alt="'+searchNickname+'@'+searchDomainFull+'">' \
' <h1>'+preferredName+'</h1>' \
' <p><b>@'+searchNickname+'@'+searchDomainFull+'</b></p>' \
' <p>'+profileDescription+'</p>'+ \
' </div>' \
'</div>'+ \
'<div class="container">\n' \
' <center>' \
' <a href="'+path+'"><button class="button"><span>Follow </span></button></a>' \
' <a href="'+path+'"><button class="button"><span>Go Back </span></button></a>' \
' </center>' \
'</div>'
result = []
i = 0
for item in parseUserFeed(session,outboxUrl,asHeader):
if not item.get('type'):
continue
if item['type']!='Create':
continue
if not item.get('object'):
continue
profileStr+= \
individualPostAsHtml(baseDir, \
session,wfRequest,personCache, \
nickname,domain,port, \
item,avatarUrl,False,False)
i+=1
if i>=20:
break
return htmlHeader(profileStyle)+profileStr+htmlFooter()