def post(self): """Perform an action.""" form = web.form("action", channel="default") response = {} if form.action == "channels": web.tx.sub.add_channel(form.name) response = {"channels": web.tx.sub.get_channels()} elif form.action == "search": response = {"results": web.tx.sub.search(form.query)} elif form.action == "preview": response = web.tx.sub.preview(form.url) elif form.action == "follow": web.tx.sub.follow(form.url) response = {"items": web.tx.sub.get_following()} web.enqueue(websub.subscribe, f"{web.tx.origin}/subscriptions/sent", form.url) elif form.action == "unfollow": pass elif form.action == "timeline": pass elif form.action == "mute": pass elif form.action == "unmute": pass elif form.action == "block": pass elif form.action == "unblock": pass web.header("Content-Type", "application/json") return response
def options(self): """Signal capabilities to CardDAV client.""" web.header("DAV", "1, 2, 3, access-control, addressbook") web.header( "Allow", "OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, " "COPY, MOVE, MKCOL, PROPFIND, PROPPATCH, LOCK, " "UNLOCK, REPORT, ACL", ) web.tx.response.naked = True return ""
def get(self): """Return a list of tokens to owner otherwise a form to submit a code.""" # TODO move to library? try: auth = tx.auth_server.get_auth_from_token( str(tx.request.headers["authorization"])) except IndexError: raise web.Forbidden("token could not be found") web.header("Content-Type", "application/json") return { "me": auth["response"]["me"], "client_id": auth["client_id"], "scope": " ".join(auth["response"]["scope"]), }
def redeem_authorization_code(flow: str, me: web.uri, name: str = None, email: str = None, photo: web.uri = None) -> dict: """ Redeem an authorization code with given `flow` and return a profile and/or a token. `flow` can be one of ['profile'][0] or ['token'][1]. [0]: https://indieauth.spec.indieweb.org/#profile-url-response [1]: https://indieauth.spec.indieweb.org/#access-token-response """ form = web.form("code", "client_id", "redirect_uri", grant_type="authorization_code") # TODO verify authenticity # TODO grant_type=refresh_token if form.grant_type not in ("authorization_code", "refresh_token"): raise web.Forbidden(f"`grant_type` {form.grant_type} not supported") auth = tx.auth_server.get_auth_from_code(form.code) if form.client_id != auth["client_id"]: raise web.BadRequest("`client_id` does not match original request") if form.redirect_uri != auth["redirect_uri"]: raise web.BadRequest("`redirect_uri` does not match original request") if "code_verifier" in form: if not auth["code_challenge"]: raise web.BadRequest("`code_verifier` without a `code_challenge`") if auth["code_challenge"] != generate_challenge(form.code_verifier): raise web.Forbidden("code mismatch") elif auth["code_challenge"]: raise web.BadRequest("`code_challenge` without `code_verifier`") response = auth["response"] if flow == "token": if not response["scope"]: raise web.BadRequest("Access Token request requires a scope") response.update( token_type="Bearer", access_token=f"secret-token:{web.nbrandom(24)}", ) response["me"] = me if "profile" in response["scope"]: response["profile"] = {"url": me, "name": name, "photo": photo} if "email" in response["scope"] and email: response["profile"]["email"] = email tx.auth_server.update_auth(response, auth["code"]) web.header("Content-Type", "application/json") return response
def get(self): """Perform an action or return an activity summary.""" try: form = web.form("action", channel="default") except web.BadRequest: return app.view.activity(web.tx.sub.get_following(), web.tx.sub.get_channels()) response = {} if form.action == "channels": response = {"channels": web.tx.sub.get_channels()} if form.action == "follow": response = {"items": web.tx.sub.get_following()} elif form.action == "timeline": response = {"items": []} web.header("Content-Type", "application/json") return response
def wrap(handler, main_app): """Ensure an owner exists and then add their details to the transaction.""" web.tx.response.claimed = True try: web.tx.host.owner = web.tx.identities.get_identity( web.tx.origin)["card"] except IndexError: web.header("Content-Type", "text/html") # if web.tx.request.method == "GET": # web.tx.response.claimed = False # raise web.NotFound(app.view.claim()) # elif web.tx.request.method == "POST": # name = web.form("name").name web.tx.identities.add_identity(web.tx.origin, "Anonymous") passphrase = " ".join(web.tx.identities.add_passphrase()) web.tx.host.owner = web.tx.user.session = web.tx.identities.get_identity( web.tx.origin)["card"] web.tx.user.is_owner = True if kiosk := web.form(kiosk=None).kiosk: with open(f"{kiosk}/passphrase", "w") as fp: fp.write(passphrase) raise web.SeeOther("/") raise web.Created(app.view.claimed(web.tx.origin, passphrase), web.tx.origin)
def put(self): """ add or update a identity """ # TODO only add if "if-none-match" is found and identity isn't try: print("if-none-match", web.tx.request.headers.if_none_match) except AttributeError: pass else: try: identities.get_identity_by_uuid(self.card_id) except ResourceNotFound: pass else: raise web.Conflict("identity already exists") # TODO only update if "if-match" matches etag on hand try: request_etag = str(web.tx.request.headers.if_match).strip('"') print("if-match", request_etag) except AttributeError: pass else: identity = identities.get_identity_by_uuid(self.card_id) current_etag = identity.get("updated", identity["published"]).timestamp() print("current etag", current_etag) if request_etag != current_etag: raise web.Conflict("previous edit already exists") # TODO non-standard type-params (url) not handled by vobject card = vobject.readOne(web.tx.request.body.decode("utf-8")) name = card.fn.value.strip() extended = {} n = card.n.value def explode(key): item = getattr(n, key) if isinstance(item, list): extended[key] = ";".join(item) else: extended[key] = [item] explode("prefix") explode("given") explode("additional") explode("family") explode("suffix") # TODO identity_type = "identity" basic = {"name": name, "uuid": self.card_id} # TODO organizations = [o.value[0] # TODO for o in card.contents.get("org", [])] # TODO for organization in organizations: # TODO if organization == name: # TODO identity_type = "organization" # TODO telephones = [] # TODO for tel in card.contents.get("tel", []): # TODO telephones.append((tel.value, tel.params["TYPE"])) # TODO websites = [] # TODO for url in card.contents.get("url", []): # TODO type = url.params.get("TYPE", []) # TODO for label in card.contents.get("x-ablabel"): # TODO if label.group == url.group: # TODO type.append(label.value) # TODO print(url.value, type) # TODO print() # TODO websites.append((url.value, type)) # photo = card.contents.get("photo")[0] # print() # print(photo) # print() # print(photo.group) # print(photo.params.get("ENCODING")) # print(photo.params.get("X-ABCROP-RECTANGLE")) # print(photo.params.get("TYPE", [])) # print(len(photo.value)) # print() # filepath = tempfile.mkstemp()[1] # with open(filepath, "wb") as fp: # fp.write(photo.value) # photo_id = canopy.branches["images"].photos.upload(filepath) # extended["photos"] = [photo_id] try: details = identities.get_identity_by_uuid(self.card_id) except ResourceNotFound: print("NEW identity!") print(basic) print(extended) quick_draft("identity", basic, publish="Identity imported from iPhone.") # XXX details = create_identity(access="private", uid=self.card_id, # XXX **basic) # XXX details = update_identity(identifier=details["identifier"], # XXX telephones=telephones, websites=websites, # XXX **extended) print("CREATED") else: print("EXISTING identity!") print(details) print("UPDATED") # XXX basic.update(extended) # XXX details = update_identity(identifier=details["identifier"], # XXX telephones=telephones, websites=websites, # XXX **basic) identity = identities.get_identity_by_uuid(self.card_id) etag = identity.get("updated", identity["published"]).timestamp() web.header("ETag", f'"{etag}"') web.tx.response.naked = True raise web.Created("created identity", f"/identities/{self.card_id}.vcf")
def get(self): """""" web.header("Content-Type", "text/vcard") return generate_vcard(self.nickname)
def propfind(self): """ Return a status listing of addressbook/contacts. This resource is requsted twice with `Depth` headers of 0 and 1. 0 is a request for the addressbook itself. 1 is a request for the addressbook itself and all contacts in the addressbook. Thus both the addressbook itself and each user have an etag. """ # TODO refactor.. web.header("DAV", "1, 2, 3, access-control, addressbook") depth = int(web.tx.request.headers["Depth"]) etags = {"": web.tx.kv["carddav-lasttouch"]} if depth == 1: for identity in get_resources("identities"): etags[identity["-uuid"]] = identity.get( "updated", identity["published"]).timestamp() props = list(web.tx.request.body.iterchildren())[0] namespaces = set() responses = [] for uuid, etag in etags.items(): ok = [] notfound = [] for prop in props.iterchildren(): # supported if prop.tag == "{DAV:}current-user-privilege-set": ok.append("""<current-user-privilege-set> <privilege> <all /> <read /> <write /> <write-properties /> <write-content /> </privilege> </current-user-privilege-set>""") if prop.tag == "{DAV:}displayname": ok.append("<displayname>carddav</displayname>") if prop.tag == "{DAV:}getetag": ok.append(f'<getetag>"{etag}"</getetag>') if prop.tag == "{DAV:}owner": ok.append("<owner>/</owner>") if prop.tag == "{DAV:}principal-URL": ok.append("""<principal-URL> <href>/identities</href> </principal-URL>""") if prop.tag == "{DAV:}principal-collection-set": ok.append("""<principal-collection-set> <href>/identities</href> </principal-collection-set>""") if prop.tag == "{DAV:}current-user-principal": ok.append("""<current-user-principal> <href>/identities</href> </current-user-principal>""") if prop.tag == "{DAV:}resourcetype": namespaces.add("CR") if uuid: ok.append("<resourcetype />") else: ok.append("""<resourcetype> <CR:addressbook /> <collection /> </resourcetype>""") if prop.tag == "{DAV:}supported-report-set": ok.append("""<supported-report-set> <supported-report> <report>principal-property-search</report> </supported-report> <supported-report> <report>sync-collection</report> </supported-report> <supported-report> <report>expand-property</report> </supported-report> <supported-report> <report>principal-search-property-set</report> </supported-report> </supported-report-set>""") if (prop.tag == "{urn:ietf:params:xml:ns:carddav}" "addressbook-home-set"): namespaces.add("CR") ok.append("""<CR:addressbook-home-set> <href>/identities</href> </CR:addressbook-home-set>""") if prop.tag == "{http://calendarserver.org/ns/}" "getctag": namespaces.add("CS") ok.append(f'<CS:getctag>"{etag}"</CS:getctag>') # conditionally supported if prop.tag == "{http://calendarserver.org/ns/}me-card": namespaces.add("CS") if uuid: notfound.append("<CS:me-card />") else: ok.append(f"""<CS:me-card> <href>/identities/{web.tx.owner["-uuid"]}.vcf</href> </CS:me-card>""") # not supported if prop.tag == "{DAV:}add-member": notfound.append("<add-member />") if prop.tag == "{DAV:}quota-available-bytes": notfound.append("<quota-available-bytes />") if prop.tag == "{DAV:}quota-used-bytes": notfound.append("<quota-used-bytes />") if prop.tag == "{DAV:}resource-id": notfound.append("<resource-id />") if prop.tag == "{DAV:}sync-token": notfound.append("<sync-token />") if prop.tag == "{urn:ietf:params:xml:ns:carddav}" "directory-gateway": namespaces.add("CR") notfound.append("<CR:directory-gateway />") if prop.tag == "{urn:ietf:params:xml:ns:carddav}" "max-image-size": namespaces.add("CR") notfound.append("<CR:max-image-size />") if prop.tag == "{urn:ietf:params:xml:ns:carddav}" "max-resource-size": namespaces.add("CR") notfound.append("<CR:max-resource-size />") if prop.tag == "{http://calendarserver.org/ns/}" "email-address-set": namespaces.add("CS") notfound.append("<CS:email-address-set />") if prop.tag == "{http://calendarserver.org/ns/}" "push-transports": namespaces.add("CS") notfound.append("<CS:push-transports />") if prop.tag == "{http://calendarserver.org/ns/}" "pushkey": namespaces.add("CS") notfound.append("<CS:pushkey />") if prop.tag == "{http://me.com/_namespace/}" "bulk-requests": namespaces.add("ME") notfound.append("<ME:bulk-requests />") href = "/identities" if uuid: href += f"/{uuid}.vcf" responses.append((href, ok, notfound)) web.tx.response.naked = True raise web.MultiStatus(view.carddav(namespaces, responses))