You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
687 lines
24 KiB
687 lines
24 KiB
// apcore is a server framework for implementing an ActivityPub application. |
|
// Copyright (C) 2019 Cory Slep |
|
// |
|
// 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/>. |
|
|
|
package main |
|
|
|
import ( |
|
"context" |
|
"fmt" |
|
"html/template" |
|
"net/http" |
|
"net/url" |
|
"strings" |
|
"time" |
|
|
|
"github.com/go-fed/activity/pub" |
|
"github.com/go-fed/activity/streams" |
|
"github.com/go-fed/activity/streams/vocab" |
|
"github.com/go-fed/apcore/app" |
|
"github.com/go-fed/apcore/paths" |
|
"github.com/go-fed/apcore/util" |
|
"github.com/google/uuid" |
|
) |
|
|
|
const ( |
|
notFoundTemplate = "not_found.tmpl" |
|
notAllowedTemplate = "not_allowed.tmpl" |
|
internalErrorTemplate = "internal_error.tmpl" |
|
badRequestTemplate = "bad_request.tmpl" |
|
loginTemplate = "login.tmpl" |
|
authTemplate = "auth.tmpl" |
|
inboxTemplate = "inbox.tmpl" |
|
outboxTemplate = "outbox.tmpl" |
|
followersTemplate = "followers.tmpl" |
|
followingTemplate = "following.tmpl" |
|
userTemplate = "user.tmpl" |
|
listUsersTemplate = "list_users.tmpl" |
|
homeTemplate = "home.tmpl" |
|
createNoteTemplate = "create_note.tmpl" |
|
listNotesTemplate = "list_notes.tmpl" |
|
noteTemplate = "note.tmpl" |
|
) |
|
|
|
var _ app.Application = &App{} |
|
var _ app.S2SApplication = &App{} |
|
var _ app.C2SApplication = &App{} |
|
|
|
var fm template.FuncMap = map[string]interface{}{ |
|
"seq": func(n int) []int { |
|
v := make([]int, n) |
|
for i := 1; i <= n; i++ { |
|
v[i-1] = i |
|
} |
|
return v |
|
}, |
|
"isString": func(i interface{}) bool { |
|
_, ok := i.(string) |
|
return ok |
|
}, |
|
} |
|
|
|
// App is an example application that minimally implements the |
|
// app.Application interface. |
|
type App struct { |
|
// startTime is set when Start is called |
|
startTime time.Time |
|
templates *template.Template |
|
} |
|
|
|
// newApplication creates a new App for the framework to use. |
|
func newApplication(glob string) (*App, error) { |
|
t, err := template.New("").Funcs(fm).ParseGlob(glob) |
|
if err != nil { |
|
return nil, err |
|
} |
|
util.InfoLogger.Infof("Templates found:") |
|
for _, tp := range t.Templates() { |
|
util.InfoLogger.Infof("%s", tp.Name()) |
|
} |
|
return &App{ |
|
templates: t, |
|
}, nil |
|
} |
|
|
|
// Start marks when the uptime began for our application. |
|
func (a *App) Start() error { |
|
a.startTime = time.Now() // Server timezone. |
|
return nil |
|
} |
|
|
|
// Stop doesn't do anything for the example application. |
|
func (a *App) Stop() error { return nil } |
|
|
|
// NewConfiguration returns our custom struct we would like to be populated by |
|
// administrators running our software. These values don't do anything in our |
|
// example application. |
|
func (a *App) NewConfiguration() interface{} { |
|
return nil |
|
} |
|
|
|
// SetConfiguration is called with the same type that is returned in |
|
// NewConfiguration, and allows us to save a copy of the values that an |
|
// administrator has configured for our software. |
|
// |
|
// Note we don't do anything with the configuration values in this example |
|
// application. But don't let that stop your imagination from taking off! |
|
func (a *App) SetConfiguration(i interface{}) error { |
|
return nil |
|
} |
|
|
|
// This example server software supports the Social API, C2S. We don't have to. |
|
// But it makes sense to support one of {C2S, S2S}, otherwise what are you |
|
// doing here? |
|
func (a *App) C2SEnabled() bool { |
|
return true |
|
} |
|
|
|
// This example server software supports the Federation API, S2S. We don't have |
|
// to. But it makes sense to support one of {C2S, S2S}, otherwise what are you |
|
// doing here? |
|
func (a *App) S2SEnabled() bool { |
|
return true |
|
} |
|
|
|
// NotFoundHandler returns our spiffy 404 page. |
|
func (a *App) NotFoundHandler(f app.Framework) http.Handler { |
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusNotFound, notFoundTemplate, nil, "NotFoundHandler") |
|
}) |
|
} |
|
|
|
// MethodNotAllowedHandler would scold the user for choosing unorthodox methods |
|
// that resulted in this error, but in this instance of the universe only sends |
|
// a boring reply. |
|
func (a *App) MethodNotAllowedHandler(f app.Framework) http.Handler { |
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusMethodNotAllowed, notAllowedTemplate, nil, "MethodNotAllowedHandler") |
|
}) |
|
} |
|
|
|
// InternalServerErrorHandler puts the underlying operating system into the time |
|
// out corner. Haha, just kidding, that was a joke. Laugh. |
|
func (a *App) InternalServerErrorHandler(f app.Framework) http.Handler { |
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
} |
|
w.WriteHeader(http.StatusInternalServerError) |
|
err = a.templates.ExecuteTemplate(w, internalErrorTemplate, a.getTemplateData(s, nil)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving InternalServerErrorHandler: %v", err) |
|
} |
|
}) |
|
} |
|
|
|
// BadRequestHandler is "I ran out of witty things to say, so I hope you |
|
// understand the pattern by now" level of error handling. Don't let this |
|
// limited example and snarky commentary demotivate you. If you wanted cold, |
|
// soulless enterprisey software, you came to the wrong place. |
|
func (a *App) BadRequestHandler(f app.Framework) http.Handler { |
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusBadRequest, badRequestTemplate, nil, "BadRequestHandler") |
|
}) |
|
} |
|
|
|
// GetLoginWebHandlerFunc returns a handler that renders the login page for |
|
// the user. |
|
// |
|
// The form should POST to "/login", and if the query parameter "login_error" |
|
// is "true" then it should also render the "email or password incorrect" error |
|
// message. |
|
func (a *App) GetLoginWebHandlerFunc(f app.Framework) http.HandlerFunc { |
|
return func(w http.ResponseWriter, r *http.Request) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, loginTemplate, nil, "GetLoginWebHandlerFunc") |
|
} |
|
} |
|
|
|
// GetAuthWebHandlerFunc returns a handler that renders the authorization page |
|
// for the user to approve in the OAuth2 flow. |
|
func (a *App) GetAuthWebHandlerFunc(f app.Framework) http.HandlerFunc { |
|
return func(w http.ResponseWriter, r *http.Request) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, authTemplate, nil, "GetAuthWebHandlerFunc") |
|
} |
|
} |
|
|
|
// GetInboxWebHandlerFunc returns a function rendering the outbox. The framework |
|
// passes in a public-only or private view of the outbox, depending on the |
|
// authorization of the incoming request. |
|
func (a *App) GetInboxWebHandlerFunc(f app.Framework) func(w http.ResponseWriter, r *http.Request, outbox vocab.ActivityStreamsOrderedCollectionPage) { |
|
return func(w http.ResponseWriter, r *http.Request, inbox vocab.ActivityStreamsOrderedCollectionPage) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, inboxTemplate, inbox, "GetInboxWebHandlerFunc") |
|
} |
|
} |
|
|
|
// GetOutboxWebHandlerFunc returns a function rendering the outbox. The |
|
// framework passes in a public-only or private view of the outbox, depending on |
|
// the authorization of the incoming request. |
|
func (a *App) GetOutboxWebHandlerFunc(f app.Framework) func(w http.ResponseWriter, r *http.Request, outbox vocab.ActivityStreamsOrderedCollectionPage) { |
|
return func(w http.ResponseWriter, r *http.Request, outbox vocab.ActivityStreamsOrderedCollectionPage) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, outboxTemplate, outbox, "GetOutboxWebHandlerFunc") |
|
} |
|
} |
|
|
|
func (a *App) GetFollowersWebHandlerFunc(f app.Framework) (app.CollectionPageHandlerFunc, app.AuthorizeFunc) { |
|
return func(w http.ResponseWriter, r *http.Request, followers vocab.ActivityStreamsCollectionPage) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, followersTemplate, followers, "GetFollowersWebHandlerFunc") |
|
}, func(c util.Context, w http.ResponseWriter, r *http.Request, db app.Database) (permit bool, err error) { |
|
return true, nil |
|
} |
|
} |
|
|
|
func (a *App) GetFollowingWebHandlerFunc(f app.Framework) (app.CollectionPageHandlerFunc, app.AuthorizeFunc) { |
|
return func(w http.ResponseWriter, r *http.Request, following vocab.ActivityStreamsCollectionPage) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, followingTemplate, following, "GetFollowingWebHandlerFunc") |
|
}, func(c util.Context, w http.ResponseWriter, r *http.Request, db app.Database) (permit bool, err error) { |
|
return true, nil |
|
} |
|
} |
|
|
|
// GetLikedWebHandlerFunc would have us fetch the user's liked collection and |
|
// then display it in a webpage. Instead, we return null so there's no way to |
|
// view the content as a webpage, but instead it is only obtainable as public |
|
// ActivityStreams data. |
|
func (a *App) GetLikedWebHandlerFunc(f app.Framework) (app.CollectionPageHandlerFunc, app.AuthorizeFunc) { |
|
return nil, nil |
|
} |
|
|
|
func (a *App) GetUserWebHandlerFunc(f app.Framework) (app.VocabHandlerFunc, app.AuthorizeFunc) { |
|
return func(w http.ResponseWriter, r *http.Request, user vocab.Type) { |
|
a.getSessionWriteTemplateHelper(w, r, f, http.StatusOK, userTemplate, user, "GetUserWebHandlerFunc") |
|
}, func(c util.Context, w http.ResponseWriter, r *http.Request, db app.Database) (permit bool, err error) { |
|
return true, nil |
|
} |
|
} |
|
|
|
// BuildRoutes takes a Router and builds the endpoint http.Handler core. |
|
// |
|
// A database handle and a supplementary Framework object are provided for |
|
// convenience and use in the server's handlers. |
|
func (a *App) BuildRoutes(r app.Router, db app.Database, f app.Framework) error { |
|
// When building routes, the framework already provides actors at the |
|
// endpoint: |
|
// |
|
// /users/{user} |
|
// |
|
// And further routes for the inbox, outbox, followers, following, and |
|
// liked collections. If you want to use web handlers at these |
|
// endpoints, other App interface functions allow you to do so. |
|
// |
|
// The framework also provides out-of-the-box OAuth2 supporting |
|
// endpoints: |
|
// |
|
// /login (GET & POST) |
|
// /logout (GET) |
|
// /authorize (GET & POST) |
|
// /token (GET) |
|
// |
|
// The framework also handles registering webfinger and host-meta |
|
// routes: |
|
// |
|
// /.well-known/host-meta |
|
// /.well-known/webfinger |
|
// |
|
// And supports using Webfinger to find actors on this server. |
|
// |
|
// Here we save copeies of our error handlers. |
|
internalErrorHandler := a.InternalServerErrorHandler(f) |
|
badRequestHandler := a.BadRequestHandler(f) |
|
|
|
// WebOnlyHandleFunc is a convenience function for endpoints with only |
|
// web content available; no ActivityStreams content exists at this |
|
// endpoint. |
|
// |
|
// It is sugar for Path(...).HandlerFunc(...) |
|
r.WebOnlyHandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
notes, err := getLatestPublicNotes(r.Context(), db) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting latest notes: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
err = a.templates.ExecuteTemplate(w, homeTemplate, a.getTemplateData(s, notes)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving home template: %v", err) |
|
} |
|
}) |
|
r.WebOnlyHandleFunc("/users", func(w http.ResponseWriter, r *http.Request) { |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
users, err := getUsers(r.Context(), db) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting latest notes: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
err = a.templates.ExecuteTemplate(w, listUsersTemplate, a.getTemplateData(s, users)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving list users template: %v", err) |
|
} |
|
}) |
|
// ActivityPubHandleFunc is a convenience function for endpoints with |
|
// only ActivityPub content; no web content exists at this endpoint. |
|
r.ActivityPubOnlyHandleFunc("/activities/{activity}", func(c util.Context, w http.ResponseWriter, r *http.Request, db app.Database) (permit bool, err error) { |
|
return true, nil |
|
}) |
|
// You can use familiar mux methods to route requests appropriately. |
|
// |
|
// Add a handler listing the latest notes we are allowed to see: |
|
// 1) Local public. |
|
// 2) Ones we have published. |
|
// 3) Ones with us in the 'to' or 'cc'. |
|
r.NewRoute().Path("/notes").Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
// View list of existing public (and maybe private) notes with |
|
// pagination. |
|
userID, authd, err := f.Validate(w, r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error validating token/creds in GET /notes: %s", err) |
|
// continue processing request as unauthenticated. |
|
} |
|
var notes []vocab.Type |
|
if err != nil || !authd { |
|
notes, err = getLatestPublicNotes(r.Context(), db) |
|
} else { |
|
userIRI := f.UserIRI(userID) |
|
notes, err = getLatestNotesAndMyPrivateNotes(r.Context(), db, userIRI.String()) |
|
} |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error getting notes: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
err = a.templates.ExecuteTemplate(w, listNotesTemplate, a.getTemplateData(s, notes)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving list notes template: %v", err) |
|
} |
|
}) |
|
// Next, a webpage to handle creating, updating, and deleting notes. |
|
// This is NOT via C2S, but is done natively in our application. |
|
r.NewRoute().Path("/notes/create").Methods("GET").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
// Ensure the user is logged in. |
|
_, authd, err := f.Validate(w, r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error validating oauth2 token in GET /notes/create: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
if !authd { |
|
http.Redirect(w, r, "/login", http.StatusFound) |
|
return |
|
} |
|
// Render the webpage. |
|
err = a.templates.ExecuteTemplate(w, createNoteTemplate, a.getTemplateData(s, nil)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving create note template: %v", err) |
|
} |
|
}) |
|
r.NewRoute().Path("/notes/create").Methods("POST").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
|
// Ensure the user is logged in. |
|
userID, authd, err := f.Validate(w, r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error validating oauth2 token in POST /notes/create: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
if !authd { |
|
http.Redirect(w, r, "/login", http.StatusFound) |
|
return |
|
} |
|
if r.Form == nil { |
|
err = r.ParseForm() |
|
if err != nil { |
|
badRequestHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
} |
|
// Parse the form |
|
toV, ok := r.Form["note_to"] |
|
if !ok || len(toV) != 1 { |
|
util.ErrorLogger.Errorf("error validating to from form") |
|
badRequestHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
to := toV[0] |
|
summaryV, ok := r.Form["note_summary"] |
|
if !ok || len(summaryV) != 1 { |
|
util.ErrorLogger.Errorf("error validating summary from form") |
|
badRequestHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
summary := summaryV[0] |
|
contentV, ok := r.Form["note_content"] |
|
if !ok || len(contentV) != 1 { |
|
util.ErrorLogger.Errorf("error validating content from form") |
|
badRequestHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
content := contentV[0] |
|
_, public := r.Form["note_public"] |
|
|
|
// Build a new Note |
|
note := streams.NewActivityStreamsNote() |
|
toProp := streams.NewActivityStreamsToProperty() |
|
tos := strings.Split(to, ",") |
|
for _, t := range tos { |
|
toIRI, err := url.Parse(t) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error validating an address in to") |
|
badRequestHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
toProp.AppendIRI(toIRI) |
|
} |
|
if public { |
|
publicIRI, err := url.Parse(pub.PublicActivityPubIRI) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error validating public ActivityStreams address") |
|
badRequestHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
toProp.AppendIRI(publicIRI) |
|
} |
|
note.SetActivityStreamsTo(toProp) |
|
summaryProp := streams.NewActivityStreamsSummaryProperty() |
|
summaryProp.AppendXMLSchemaString(summary) |
|
note.SetActivityStreamsSummary(summaryProp) |
|
contentProp := streams.NewActivityStreamsContentProperty() |
|
contentProp.AppendXMLSchemaString(content) |
|
note.SetActivityStreamsContent(contentProp) |
|
ctx := util.Context{r.Context()} |
|
// Send the note -- a Create will automatically be created |
|
if err := f.Send(ctx, paths.UUID(userID), note); err != nil { |
|
util.ErrorLogger.Errorf("error sending when creating note: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
iri := note.GetJSONLDId().GetIRI() |
|
http.Redirect(w, r, iri.String(), http.StatusFound) |
|
}) |
|
// First, we need an authentication function to make sure whoever views |
|
// the ActivityStreams data has proper credentials to view the web or |
|
// ActivityStreams data. |
|
authFn := func(c util.Context, w http.ResponseWriter, r *http.Request, db app.Database) (permit bool, err error) { |
|
// Determine who, if any, is logged-in. |
|
userID, authd, err := f.Validate(w, r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error validating token/creds in GET /notes: %s", err) |
|
// continue processing request as unauthenticated. |
|
} |
|
ctx := f.Context(r) |
|
noteID, err := ctx.CompleteRequestURL() |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error sending when creating note: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
if err == nil && authd { |
|
userIRI := f.UserIRI(userID) |
|
// Authenticated request |
|
permit, err = getNoteIsReadable(ctx, db, noteID, userIRI) |
|
} else { |
|
// Unauthenticated request |
|
permit, err = getNoteIsPublic(ctx, db, noteID) |
|
} |
|
return |
|
} |
|
// Next, we use the auth function to protect the note. |
|
r.ActivityPubAndWebHandleFunc("/notes/{note}", authFn, func(w http.ResponseWriter, r *http.Request) { |
|
// View note in web page. |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
ctx := f.Context(r) |
|
noteID, err := ctx.CompleteRequestURL() |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error sending when creating note: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
vt, err := f.GetByIRI(ctx, noteID) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("error fetching note: %s", err) |
|
internalErrorHandler.ServeHTTP(w, r) |
|
return |
|
} |
|
err = a.templates.ExecuteTemplate(w, noteTemplate, a.getTemplateData(s, vt)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving note template: %v", err) |
|
} |
|
}) |
|
return nil |
|
} |
|
|
|
func (a *App) NewIDPath(c context.Context, t vocab.Type) (path string, err error) { |
|
switch t.GetTypeName() { |
|
case "Note": |
|
path = fmt.Sprintf("/notes/%s", uuid.New().String()) |
|
case "Create": |
|
path = fmt.Sprintf("/activities/%s", uuid.New().String()) |
|
default: |
|
err = fmt.Errorf("NewID unhandled type name: %s", t.GetTypeName()) |
|
} |
|
return |
|
} |
|
|
|
// ApplyFederatingCallbacks lets us provide hooks for our application based on |
|
// incoming ActivityStreams data from peer servers. |
|
func (a *App) ApplyFederatingCallbacks(fwc *pub.FederatingWrappedCallbacks) (others []interface{}) { |
|
// Here, we add additional behavior to our application if we receive a |
|
// federated Create activity, besides the spec-suggested side effects. |
|
// |
|
// The additional behavior of our application is to print out the |
|
// Create activity to Stdout. |
|
fwc.Create = func(c context.Context, create vocab.ActivityStreamsCreate) error { |
|
fmt.Println(streams.Serialize(create)) |
|
return nil |
|
} |
|
// Here we add new behavior to our application. |
|
// |
|
// The new behavior is to print out Listen activities to Stdout. |
|
others = []interface{}{ |
|
func(c context.Context, listen vocab.ActivityStreamsListen) error { |
|
fmt.Println(streams.Serialize(listen)) |
|
return nil |
|
}, |
|
} |
|
return |
|
} |
|
|
|
// ApplySocialCallbacks lets us provide hooks for our application based on |
|
// incoming ActivityStreams data from a user's ActivityPub client. |
|
func (a *App) ApplySocialCallbacks(swc *pub.SocialWrappedCallbacks) (others []interface{}) { |
|
// Here we add no new C2S Behavior. Doing nothing in this function will |
|
// let the framework handle the suggested C2S side effects. |
|
return |
|
} |
|
|
|
// ScopePermitsPostOutbox ensures the OAuth2 token scope is "loggedin", which |
|
// is the only permission. Other applications can have more granular |
|
// authorization systems. |
|
func (a *App) ScopePermitsPostOutbox(scope string) (permitted bool, err error) { |
|
return scope == "postOutbox" || scope == "all", nil |
|
} |
|
|
|
// ScopePermitsPrivateGetInbox ensures the OAuth2 token scope is "loggedin", |
|
// which is the only permission. Other applications can have more granular |
|
// authorization systems. |
|
func (a *App) ScopePermitsPrivateGetInbox(scope string) (permitted bool, err error) { |
|
return scope == "getInbox" || scope == "all", nil |
|
} |
|
|
|
// ScopePermitsPrivateGetOutbox ensures the OAuth2 token scope is "loggedin", |
|
// which is the only permission. Other applications can have more granular |
|
// authorization systems. |
|
func (a *App) ScopePermitsPrivateGetOutbox(scope string) (permitted bool, err error) { |
|
return scope == "getOutbox" || scope == "all", nil |
|
} |
|
|
|
// Software describes the current running software, based on the code. This |
|
// allows everyone, from users to developers, to make reasonable judgments about |
|
// the state of the Federative ecosystem as a whole. |
|
// |
|
// Warning: Nothing inherently prevents your application from lying and |
|
// attempting to masquerade as another set of software. Don't be that jerk. |
|
func (a *App) Software() app.Software { |
|
return app.Software{ |
|
Name: "kama", |
|
MajorVersion: 0, |
|
MinorVersion: 1, |
|
PatchVersion: 0, |
|
} |
|
} |
|
|
|
func (a *App) DefaultUserPreferences() interface{} { |
|
return nil |
|
} |
|
|
|
func (a *App) DefaultUserPrivileges() interface{} { |
|
return nil |
|
} |
|
func (a *App) DefaultAdminPrivileges() interface{} { |
|
return nil |
|
} |
|
|
|
// This is a helper function to generate common data needed in the web |
|
// templates. |
|
func (a *App) getTemplateData(s app.Session, other interface{}) map[string]interface{} { |
|
if vt, ok := other.(vocab.Type); ok { |
|
svt, err := streams.Serialize(vt) |
|
if err == nil { |
|
other = svt |
|
} else { |
|
util.ErrorLogger.Errorf("error serializing ActivityStreams for rendering: %s", err) |
|
} |
|
} |
|
if vts, ok := other.([]vocab.Type); ok { |
|
var svts []map[string]interface{} |
|
for _, vt := range vts { |
|
svt, err := streams.Serialize(vt) |
|
if err == nil { |
|
svts = append(svts, svt) |
|
} else { |
|
util.ErrorLogger.Errorf("error serializing ActivityStreams for rendering: %s", err) |
|
} |
|
} |
|
other = svts |
|
} |
|
|
|
m := map[string]interface{}{ |
|
"Other": other, |
|
"Nav": []struct { |
|
Href string |
|
Name string |
|
}{ |
|
{ |
|
Href: "/", |
|
Name: "home", |
|
}, |
|
{ |
|
Href: "/users", |
|
Name: "users", |
|
}, |
|
{ |
|
Href: "/notes", |
|
Name: "notes", |
|
}, |
|
}, |
|
} |
|
if s != nil { |
|
user, err := s.UserID() |
|
if err == nil && len(user) > 0 { |
|
m["User"] = user |
|
} |
|
} |
|
return m |
|
} |
|
|
|
func (a *App) getSessionWriteTemplateHelper(w http.ResponseWriter, r *http.Request, f app.Framework, code int, tmpl string, data interface{}, debug string) { |
|
s, err := f.Session(r) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error getting session: %v", err) |
|
a.InternalServerErrorHandler(f).ServeHTTP(w, r) |
|
return |
|
} |
|
w.WriteHeader(code) |
|
err = a.templates.ExecuteTemplate(w, tmpl, a.getTemplateData(s, data)) |
|
if err != nil { |
|
util.ErrorLogger.Errorf("Error serving %s: %v", debug, err) |
|
} |
|
}
|
|
|