Browse Source
* Support webfinger requests for the live account. Closes https://github.com/owncast/owncast/issues/1193
* Support for actor requests. Returns response for live actor. Closes https://github.com/owncast/owncast/issues/1203
* Handle follow and unfollow requests. Closes
https://github.com/owncast/owncast/issues/1191 and https://github.com/owncast/owncast/issues/1205 and https://github.com/owncast/owncast/issues/1206 and https://github.com/owncast/owncast/issues/1194
* Add basic support for sending out text activities. For https://github.com/owncast/owncast/issues/1192
* Some error handling and passing of dynamic local account names.
* Add hardcoded example image attachment to test post
* Centralize the map of accounts and inboxes
* No longer disable the preview generator based on YP toggle
* Send a federated message to followers when stream starts. For https://github.com/owncast/owncast/issues/1192
* Placeholder for attaching tags
* Add image description
* Save and get to outbox persistence. Return using outbox endpoint for actor
* Pass payloads to be handled through the gochan
* Handle undo follow requests explitly, not all undo requests
* Add API for manually sending simple federated messages. Closes #1215
* Verify inbox requests. Closes #1321
* Add route to fetch a single AP object by ID. For #1329
* Add responses to fediverse nodeinfo requests
* Set and get federation config values for admin
* Handle host-meta requests
* Do not send out message if disabled. Use saved go live message.
* Require AP-compatible content types for AP-related requests
* Rename ap models to apmodels for clarity
* Change how content type matching takes place.
* io -> ioutil
* Add stub delete activity callback
* Handle likes and announces to surface engagement in chat. Part of #1229
* Append url to go live posts
* Do not require specific content types for nodeinfo requests
* Add follow engagement chat message via AP
* add owncast user-agent to requests
* Set note visibility to public (for now)
* Fix saving/fetching a single object
* Add support for x-nodeinfo2 responses
* Point to the dev admin branch for ap
* Bundle in dev admin for testing
* Add error logging
* Add AP middleware back
* Point to the new external compatible logo endpoint
* Clean up more AP logging to help testing
* Tweak go live text and link hashtags
* Fix bug in fetching init time
* Send update actor activities when server details/profile is updated
* Add federation config overview to web client config
* Add additional actor properties
* Make the AP middleware checking more flexible when looking at types
* First pass at remote fediverse follow flow. For #1371
* Added a basic AP actor followers endpoint
* WIP client followers API
* Add profile-page reference to webfinger response
* Add aliases to webfinger response
* Fix content-type returned to be expected activitypub+json
* First pass at followers api
* Point at local dev copy of go-fed/activity
* Add custom toot Hashtag objects to posts
* Store additional user details to followers table
* Fix AP followers endpoint. Closes #1204
* Add owncast hashtag as an invisible tag to go live posts
* Reject AP requests when it is disabled
* Add actor util for generating full account user from person object
* Verify inbox requests before performing any other work
* Accept actor update requests
* Fix linter errors in federation branch
* Migrate AP SQL to sqlc for type safe queries
* Use the @unclearParadigm REST parameter helper
* Fix verifying post ID on AP engagement
* WIP privacy/request approval
* Style the remote follow modal
* First pass at a followers list component w/ mock data. #1370
* Revert "Use the @unclearParadigm REST parameter helper"
This reverts commit c8af8a413f
.
* Fix get followers API
* Add support for requiring approval. Closes https://github.com/owncast/owncast/issues/1208
* Handle Applications as Actors partly for PeerTube support
* add temp todo list
* check route on load, this might change later
* style followers
* account for just 1 tab case
* Remove mock data. Allow showing follow button even when there are no external actions defined
* Point to actual followers API
* Support fallback img for follower views
* Remove duplicate verification. Add some additional verbose logging
* Bundle dev admin
* Add type to host-meta webfinger template response
* Tweak remote follow modal content
* WIP federation followers refactor
* Do not send pointer to middleware
* Update admin
* Add setting for toggling displaying fediverse engagement. Closes #1404
* Add in-development admin
* Do not enable cors on admin followers api
* Add db migration for updating messages table
* Enable empty string go live messages to disable
* Remove debug messages
* Rework some ActivityPub handling.
Create new Actor->Person handling.
Create new Actor->Service handling.
Add engagement handlers to send chat events and store event objects.
Store inbound activities to new ap_inbound_activities table.
* Support federated engagement events.
Store them in the messages table and surface them via chat events.
* Support federated event engatement in the chat
* Tweak web UI followers handling
* Point go.mod at remote fork instead of local
* Update admin
* Merged in develop. Couple fixes
* Update dev admin
* Update fedi engagement posts.
- Fix incorrect action text.
- Add action icons.
* Set public as to instead of cc for ap msg
* Updated styling for federated actions in chat
* Add support for blocking federated domains. Closes #1209
* Force checking of https in verify step
* Update dev admin
* Return user scopes in chat history api. Closes #1586
* Update dev admin
* Add AP outbound request worker pool. Closes #1571
* Disable (temporarily?) owncast tag on AP posts
* Consolidate creating activity+notes in outbound AP messages
* Add inbox worker pool. Closes #1570
* Update dev admin bundle
* Clean up some logs
* Re-enable inbound verfication
* Save full IRI to outbox instead of path
* Reject if full IRI is not found in outbox
* Use full ActivityPub user account in chat event
* Fix and expand follower APIs
- Add missing IDs to AP follower endpoints
- Split AP follower endpoints into initial request and pages.
- Support pagination in AP requests.
* Include IRI in error message
* Hide chat toggle when chat is hidden. Closes #1606
* Updates to followers pagination
* Set default go live message
* Remove log
* indirect -> direct import
* Updates for inbound federated event handling.
- Keep track of existing events and reject duplicates.
- Change what is sent to chat for surfing federated engagement.
- Keep track if outbound events are automated "go live" events or not.
* Update chat federated engagement.
* Update dev admin.
* Move from being a person to a bot (service). Closes #1619
* Only set server init date if not already set
* Only save notes to outbox able
* Rework private-mode followers/approvals
* API for returning a list of federated actions for #1573
* Fix too-small follower cells and jumpy tabs. Closes #1616 and closes #1516
* Fix shortcuts getting fired on inputs. Fixes #1489 and #1201
* Add spinner, autoclose + other fixes to follow modal. Fixes #1593
* Fix fetching a single object by IRI
* SendFederationMessage -> SendFederatedMessage
* Autolink and create tag objects from manual posts. Closes #1620
* Update dev admin bundle
* Handle engagement from non-automated/live posts
* Reject federated engagement actions if they do not match a local post
* Update dev admin bundle
* A bunch of cleanup
* Fix unused assignments and logic
* Remove unused function
* Add content warning and sentive content flag if stream is NSFW. Closes #1624
* Disable fetching objects by IRI when in private mode. Closes #1623
* Update the error message of the remote follow dialog. closes #1622
* Update dev admin
* Fix NREs throwing in test content
* Fix query that wasn't properly filtering out hidden messages
* Test against user being disabled instead of message visibility
* Fix automated test NRE
* Update comment
* Adjust federated engagement chat views. Closes #1617
* Add additional index to users table
* Add support for removing followers/requests. Closes #1630
* Reject federated actions from blocked actors. #1631
* Use fallback avatar if it fails to load. Closes #1635
* Fix styling of follower list. Closes #1636
* Add basic blurb stating they should follow the server. Closes #1641
* Update dev admin
* Set default go live message in migration. Closes #1642
* Reset the messages table on 0.0.11 schema migration
* Fix js error with moderation actions. Closes #1621
* Add a bit more clarification on follow modal. Closes #1599
* Remove todos
* Split out actor and domain blocking checks
* Check for errors on default values being set
* Clean up actor rejection due to being blocked
* Update dev admin
* Add colon to error to make it easier to read
* Remove markdown rendering of go live message. Reorganize text. Remove content warning. Closes #1645
* Break out the sort+render messages logic so it can be fired on visibility change. Closes #1643
* Do not send profile updates if federation is disabled
* Save follow references to inbound activities table
* Update dev admin
* Add blocked actor test
* Remove the overloaded term of Follow from social links
* Fix test running in memory only
* Remove "just" in engagement messags
* Replace star with heart for like action.
* Update dev admin
* Explicitly set cc as public
* Remove overly using the stream name in fediverse engagement messages
* Some federated/follow UI tweaks
* Remove explicit cc and bcc as they are not required
* Explicitly set the audience
* Remove extra margin
* Add Join Fediverse button to follow modal. Closes #1651
* Do not allow multiple follows to send multiple events. Closes #1650
* Give events a min height
* Do not allow old posts to be liked/shared. Closes #1652
* Remove value from log message
* Alert followers on private mode toggle
* Ignore clicks to follow button if disabled
* Remove underline from action buttons
* Add moderator icon to join message
* Update admin
* Post-merge remove unused var
* Remove pointing at feature branch
Co-authored-by: Ginger Wong <omqmail@gmail.com>
pull/1678/head
174 changed files with 7296 additions and 405 deletions
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
package activitypub |
||||
|
||||
import ( |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/activitypub/inbox" |
||||
"github.com/owncast/owncast/activitypub/outbox" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/workerpool" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/models" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// Start will initialize and start the federation support.
|
||||
func Start(datastore *data.Datastore) { |
||||
persistence.Setup(datastore) |
||||
workerpool.InitOutboundWorkerPool() |
||||
inbox.InitInboxWorkerPool() |
||||
StartRouter() |
||||
|
||||
// Test
|
||||
if data.GetPrivateKey() == "" { |
||||
privateKey, publicKey, err := crypto.GenerateKeys() |
||||
_ = data.SetPrivateKey(string(privateKey)) |
||||
_ = data.SetPublicKey(string(publicKey)) |
||||
if err != nil { |
||||
log.Errorln("Unable to get private key", err) |
||||
} |
||||
} |
||||
} |
||||
|
||||
// SendLive will send a "Go Live" message to followers.
|
||||
func SendLive() error { |
||||
return outbox.SendLive() |
||||
} |
||||
|
||||
// SendPublicFederatedMessage will send an arbitrary provided message to followers.
|
||||
func SendPublicFederatedMessage(message string) error { |
||||
return outbox.SendPublicMessage(message) |
||||
} |
||||
|
||||
// GetFollowerCount will return the local tracked follower count.
|
||||
func GetFollowerCount() (int64, error) { |
||||
return persistence.GetFollowerCount() |
||||
} |
||||
|
||||
// GetPendingFollowRequests will return the pending follow requests.
|
||||
func GetPendingFollowRequests() ([]models.Follower, error) { |
||||
return persistence.GetPendingFollowRequests() |
||||
} |
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"net/url" |
||||
"time" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
// PrivacyAudience represents the audience for an activity.
|
||||
type PrivacyAudience = string |
||||
|
||||
const ( |
||||
// PUBLIC is an audience meaning anybody can view the item.
|
||||
PUBLIC PrivacyAudience = "https://www.w3.org/ns/activitystreams#Public" |
||||
) |
||||
|
||||
// MakeCreateActivity will return a new Create activity with the provided ID.
|
||||
func MakeCreateActivity(activityID *url.URL) vocab.ActivityStreamsCreate { |
||||
activity := streams.NewActivityStreamsCreate() |
||||
id := streams.NewJSONLDIdProperty() |
||||
id.Set(activityID) |
||||
activity.SetJSONLDId(id) |
||||
|
||||
// CC the public if we're not treating ActivityPub as "private".
|
||||
if !data.GetFederationIsPrivate() { |
||||
public, _ := url.Parse(PUBLIC) |
||||
to := streams.NewActivityStreamsToProperty() |
||||
to.AppendIRI(public) |
||||
activity.SetActivityStreamsTo(to) |
||||
|
||||
audience := streams.NewActivityStreamsAudienceProperty() |
||||
audience.AppendIRI(public) |
||||
activity.SetActivityStreamsAudience(audience) |
||||
} |
||||
|
||||
return activity |
||||
} |
||||
|
||||
// MakeUpdateActivity will return a new Update activity with the provided aID.
|
||||
func MakeUpdateActivity(activityID *url.URL) vocab.ActivityStreamsUpdate { |
||||
activity := streams.NewActivityStreamsUpdate() |
||||
id := streams.NewJSONLDIdProperty() |
||||
id.Set(activityID) |
||||
activity.SetJSONLDId(id) |
||||
|
||||
// CC the public if we're not treating ActivityPub as "private".
|
||||
if !data.GetFederationIsPrivate() { |
||||
public, _ := url.Parse(PUBLIC) |
||||
cc := streams.NewActivityStreamsCcProperty() |
||||
cc.AppendIRI(public) |
||||
activity.SetActivityStreamsCc(cc) |
||||
} |
||||
|
||||
return activity |
||||
} |
||||
|
||||
// MakeNote will return a new Note object.
|
||||
func MakeNote(text string, noteIRI *url.URL, attributedToIRI *url.URL) vocab.ActivityStreamsNote { |
||||
note := streams.NewActivityStreamsNote() |
||||
content := streams.NewActivityStreamsContentProperty() |
||||
content.AppendXMLSchemaString(text) |
||||
note.SetActivityStreamsContent(content) |
||||
id := streams.NewJSONLDIdProperty() |
||||
id.Set(noteIRI) |
||||
note.SetJSONLDId(id) |
||||
|
||||
published := streams.NewActivityStreamsPublishedProperty() |
||||
published.Set(time.Now()) |
||||
note.SetActivityStreamsPublished(published) |
||||
|
||||
attributedTo := attributedToIRI |
||||
attr := streams.NewActivityStreamsAttributedToProperty() |
||||
attr.AppendIRI(attributedTo) |
||||
note.SetActivityStreamsAttributedTo(attr) |
||||
|
||||
// CC the public if we're not treating ActivityPub as "private".
|
||||
if !data.GetFederationIsPrivate() { |
||||
public, _ := url.Parse(PUBLIC) |
||||
cc := streams.NewActivityStreamsCcProperty() |
||||
cc.AppendIRI(public) |
||||
note.SetActivityStreamsCc(cc) |
||||
} |
||||
|
||||
return note |
||||
} |
@ -0,0 +1,263 @@
@@ -0,0 +1,263 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/url" |
||||
"time" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/models" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// ActivityPubActor represents a single actor in handling ActivityPub activity.
|
||||
type ActivityPubActor struct { |
||||
// ActorIRI is the IRI of the remote actor.
|
||||
ActorIri *url.URL |
||||
// FollowRequestIRI is the unique identifier of the follow request.
|
||||
FollowRequestIri *url.URL |
||||
// Inbox is the inbox URL of the remote follower
|
||||
Inbox *url.URL |
||||
// Name is the display name of the follower.
|
||||
Name string |
||||
// Username is the account username of the remote actor.
|
||||
Username string |
||||
// FullUsername is the username@account.tld representation of the user.
|
||||
FullUsername string |
||||
// Image is the avatar image of the Actor.
|
||||
Image *url.URL |
||||
// W3IDSecurityV1PublicKey is the public key of the actor.
|
||||
W3IDSecurityV1PublicKey vocab.W3IDSecurityV1PublicKeyProperty |
||||
// DisabledAt is the time, if any, this follower was blocked/removed.
|
||||
DisabledAt *time.Time |
||||
} |
||||
|
||||
// DeleteRequest represents a request for delete.
|
||||
type DeleteRequest struct { |
||||
ActorIri string |
||||
} |
||||
|
||||
// MakeActorFromPerson takes a full ActivityPub Person and returns our internal
|
||||
// representation of an actor.
|
||||
func MakeActorFromPerson(person vocab.ActivityStreamsPerson) ActivityPubActor { |
||||
apActor := ActivityPubActor{ |
||||
ActorIri: person.GetJSONLDId().Get(), |
||||
Inbox: person.GetActivityStreamsInbox().GetIRI(), |
||||
Name: person.GetActivityStreamsName().Begin().GetXMLSchemaString(), |
||||
Username: person.GetActivityStreamsPreferredUsername().GetXMLSchemaString(), |
||||
FullUsername: GetFullUsernameFromPerson(person), |
||||
W3IDSecurityV1PublicKey: person.GetW3IDSecurityV1PublicKey(), |
||||
} |
||||
|
||||
if person.GetActivityStreamsIcon() != nil && person.GetActivityStreamsIcon().Len() > 0 { |
||||
apActor.Image = person.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI() |
||||
} |
||||
|
||||
return apActor |
||||
} |
||||
|
||||
// MakeActorFromService takes a full ActivityPub Service and returns our internal
|
||||
// representation of an actor.
|
||||
func MakeActorFromService(service vocab.ActivityStreamsService) ActivityPubActor { |
||||
apActor := ActivityPubActor{ |
||||
ActorIri: service.GetJSONLDId().Get(), |
||||
Inbox: service.GetActivityStreamsInbox().GetIRI(), |
||||
Name: service.GetActivityStreamsName().Begin().GetXMLSchemaString(), |
||||
Username: service.GetActivityStreamsPreferredUsername().GetXMLSchemaString(), |
||||
FullUsername: GetFullUsernameFromService(service), |
||||
W3IDSecurityV1PublicKey: service.GetW3IDSecurityV1PublicKey(), |
||||
} |
||||
|
||||
if service.GetActivityStreamsIcon() != nil && service.GetActivityStreamsIcon().Len() > 0 { |
||||
apActor.Image = service.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI() |
||||
} |
||||
|
||||
return apActor |
||||
} |
||||
|
||||
// MakeActorPropertyWithID will return an actor property filled with the provided IRI.
|
||||
func MakeActorPropertyWithID(idIRI *url.URL) vocab.ActivityStreamsActorProperty { |
||||
actor := streams.NewActivityStreamsActorProperty() |
||||
actor.AppendIRI(idIRI) |
||||
return actor |
||||
} |
||||
|
||||
// MakeServiceForAccount will create a new local actor service with the the provided username.
|
||||
func MakeServiceForAccount(accountName string) vocab.ActivityStreamsService { |
||||
actorIRI := MakeLocalIRIForAccount(accountName) |
||||
|
||||
person := streams.NewActivityStreamsService() |
||||
nameProperty := streams.NewActivityStreamsNameProperty() |
||||
nameProperty.AppendXMLSchemaString(data.GetServerName()) |
||||
person.SetActivityStreamsName(nameProperty) |
||||
|
||||
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty() |
||||
preferredUsernameProperty.SetXMLSchemaString(accountName) |
||||
person.SetActivityStreamsPreferredUsername(preferredUsernameProperty) |
||||
|
||||
inboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/inbox") |
||||
|
||||
inboxProp := streams.NewActivityStreamsInboxProperty() |
||||
inboxProp.SetIRI(inboxIRI) |
||||
person.SetActivityStreamsInbox(inboxProp) |
||||
|
||||
needsFollowApprovalProperty := streams.NewActivityStreamsManuallyApprovesFollowersProperty() |
||||
needsFollowApprovalProperty.Set(data.GetFederationIsPrivate()) |
||||
person.SetActivityStreamsManuallyApprovesFollowers(needsFollowApprovalProperty) |
||||
|
||||
outboxIRI := MakeLocalIRIForResource("/user/" + accountName + "/outbox") |
||||
|
||||
outboxProp := streams.NewActivityStreamsOutboxProperty() |
||||
outboxProp.SetIRI(outboxIRI) |
||||
person.SetActivityStreamsOutbox(outboxProp) |
||||
|
||||
id := streams.NewJSONLDIdProperty() |
||||
id.Set(actorIRI) |
||||
person.SetJSONLDId(id) |
||||
|
||||
publicKey := crypto.GetPublicKey(actorIRI) |
||||
|
||||
publicKeyProp := streams.NewW3IDSecurityV1PublicKeyProperty() |
||||
publicKeyType := streams.NewW3IDSecurityV1PublicKey() |
||||
|
||||
pubKeyIDProp := streams.NewJSONLDIdProperty() |
||||
pubKeyIDProp.Set(publicKey.ID) |
||||
|
||||
publicKeyType.SetJSONLDId(pubKeyIDProp) |
||||
|
||||
ownerProp := streams.NewW3IDSecurityV1OwnerProperty() |
||||
ownerProp.SetIRI(publicKey.Owner) |
||||
publicKeyType.SetW3IDSecurityV1Owner(ownerProp) |
||||
|
||||
publicKeyPemProp := streams.NewW3IDSecurityV1PublicKeyPemProperty() |
||||
publicKeyPemProp.Set(publicKey.PublicKeyPem) |
||||
publicKeyType.SetW3IDSecurityV1PublicKeyPem(publicKeyPemProp) |
||||
publicKeyProp.AppendW3IDSecurityV1PublicKey(publicKeyType) |
||||
person.SetW3IDSecurityV1PublicKey(publicKeyProp) |
||||
|
||||
if t, err := data.GetServerInitTime(); t != nil { |
||||
publishedDateProp := streams.NewActivityStreamsPublishedProperty() |
||||
publishedDateProp.Set(t.Time) |
||||
person.SetActivityStreamsPublished(publishedDateProp) |
||||
} else { |
||||
log.Errorln("unable to fetch server init time", err) |
||||
} |
||||
|
||||
// Profile properties
|
||||
|
||||
// Avatar
|
||||
userAvatarURLString := data.GetServerURL() + "/logo/external" |
||||
userAvatarURL, err := url.Parse(userAvatarURLString) |
||||
if err != nil { |
||||
log.Errorln("unable to parse user avatar url", userAvatarURLString, err) |
||||
} |
||||
|
||||
image := streams.NewActivityStreamsImage() |
||||
imgProp := streams.NewActivityStreamsUrlProperty() |
||||
imgProp.AppendIRI(userAvatarURL) |
||||
image.SetActivityStreamsUrl(imgProp) |
||||
icon := streams.NewActivityStreamsIconProperty() |
||||
icon.AppendActivityStreamsImage(image) |
||||
person.SetActivityStreamsIcon(icon) |
||||
|
||||
// Actor URL
|
||||
urlProperty := streams.NewActivityStreamsUrlProperty() |
||||
urlProperty.AppendIRI(actorIRI) |
||||
person.SetActivityStreamsUrl(urlProperty) |
||||
|
||||
// Profile header
|
||||
headerImage := streams.NewActivityStreamsImage() |
||||
headerImgPropURL := streams.NewActivityStreamsUrlProperty() |
||||
headerImgPropURL.AppendIRI(userAvatarURL) |
||||
headerImage.SetActivityStreamsUrl(headerImgPropURL) |
||||
headerImageProp := streams.NewActivityStreamsImageProperty() |
||||
headerImageProp.AppendActivityStreamsImage(headerImage) |
||||
person.SetActivityStreamsImage(headerImageProp) |
||||
|
||||
// Profile bio
|
||||
summaryProperty := streams.NewActivityStreamsSummaryProperty() |
||||
summaryProperty.AppendXMLSchemaString(data.GetServerSummary()) |
||||
person.SetActivityStreamsSummary(summaryProperty) |
||||
|
||||
// Links
|
||||
for _, link := range data.GetSocialHandles() { |
||||
addMetadataLinkToProfile(person, link.Platform, link.URL) |
||||
} |
||||
|
||||
// Discoverable
|
||||
discoverableProperty := streams.NewTootDiscoverableProperty() |
||||
discoverableProperty.Set(true) |
||||
person.SetTootDiscoverable(discoverableProperty) |
||||
|
||||
// Followers
|
||||
followersProperty := streams.NewActivityStreamsFollowersProperty() |
||||
followersURL := *actorIRI |
||||
followersURL.Path = actorIRI.Path + "/followers" |
||||
followersProperty.SetIRI(&followersURL) |
||||
person.SetActivityStreamsFollowers(followersProperty) |
||||
|
||||
// Tags
|
||||
tagProp := streams.NewActivityStreamsTagProperty() |
||||
for _, tagString := range data.GetServerMetadataTags() { |
||||
hashtag := MakeHashtag(tagString) |
||||
tagProp.AppendTootHashtag(hashtag) |
||||
} |
||||
|
||||
person.SetActivityStreamsTag(tagProp) |
||||
|
||||
// Work around an issue where a single attachment will not serialize
|
||||
// as an array, so add another item to the mix.
|
||||
if len(data.GetSocialHandles()) == 1 { |
||||
addMetadataLinkToProfile(person, "Owncast", "https://owncast.online") |
||||
} |
||||
|
||||
return person |
||||
} |
||||
|
||||
// GetFullUsernameFromPerson will return the user@host.tld formatted user given a person object.
|
||||
func GetFullUsernameFromPerson(person vocab.ActivityStreamsPerson) string { |
||||
hostname := person.GetJSONLDId().GetIRI().Hostname() |
||||
username := person.GetActivityStreamsPreferredUsername().GetXMLSchemaString() |
||||
fullUsername := fmt.Sprintf("%s@%s", username, hostname) |
||||
|
||||
return fullUsername |
||||
} |
||||
|
||||
// GetFullUsernameFromService will return the user@host.tld formatted user given a service object.
|
||||
func GetFullUsernameFromService(person vocab.ActivityStreamsService) string { |
||||
hostname := person.GetJSONLDId().GetIRI().Hostname() |
||||
username := person.GetActivityStreamsPreferredUsername().GetXMLSchemaString() |
||||
fullUsername := fmt.Sprintf("%s@%s", username, hostname) |
||||
|
||||
return fullUsername |
||||
} |
||||
|
||||
func addMetadataLinkToProfile(profile vocab.ActivityStreamsService, name string, url string) { |
||||
attachments := profile.GetActivityStreamsAttachment() |
||||
if attachments == nil { |
||||
attachments = streams.NewActivityStreamsAttachmentProperty() |
||||
} |
||||
|
||||
displayName := name |
||||
socialHandle := models.GetSocialHandle(name) |
||||
if socialHandle != nil { |
||||
displayName = socialHandle.Platform |
||||
} |
||||
|
||||
linkValue := fmt.Sprintf("<a href=\"%s\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\">%s</a>", url, url) |
||||
|
||||
attachment := streams.NewActivityStreamsObject() |
||||
attachmentProp := streams.NewJSONLDTypeProperty() |
||||
attachmentProp.AppendXMLSchemaString("PropertyValue") |
||||
attachment.SetJSONLDType(attachmentProp) |
||||
attachmentName := streams.NewActivityStreamsNameProperty() |
||||
attachmentName.AppendXMLSchemaString(displayName) |
||||
attachment.SetActivityStreamsName(attachmentName) |
||||
attachment.GetUnknownProperties()["value"] = linkValue |
||||
|
||||
attachments.AppendActivityStreamsObject(attachment) |
||||
profile.SetActivityStreamsAttachment(attachments) |
||||
} |
@ -0,0 +1,168 @@
@@ -0,0 +1,168 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"io/ioutil" |
||||
"net/url" |
||||
"os" |
||||
"testing" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
func makeFakeService() vocab.ActivityStreamsService { |
||||
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo") |
||||
name := "Mr Foo" |
||||
username := "foodawg" |
||||
inbox, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/inbox") |
||||
userAvatarURL, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/avatar.png") |
||||
|
||||
service := streams.NewActivityStreamsService() |
||||
|
||||
id := streams.NewJSONLDIdProperty() |
||||
id.Set(iri) |
||||
service.SetJSONLDId(id) |
||||
|
||||
nameProperty := streams.NewActivityStreamsNameProperty() |
||||
nameProperty.AppendXMLSchemaString(name) |
||||
service.SetActivityStreamsName(nameProperty) |
||||
|
||||
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty() |
||||
preferredUsernameProperty.SetXMLSchemaString(username) |
||||
service.SetActivityStreamsPreferredUsername(preferredUsernameProperty) |
||||
|
||||
inboxProp := streams.NewActivityStreamsInboxProperty() |
||||
inboxProp.SetIRI(inbox) |
||||
service.SetActivityStreamsInbox(inboxProp) |
||||
|
||||
image := streams.NewActivityStreamsImage() |
||||
imgProp := streams.NewActivityStreamsUrlProperty() |
||||
imgProp.AppendIRI(userAvatarURL) |
||||
image.SetActivityStreamsUrl(imgProp) |
||||
icon := streams.NewActivityStreamsIconProperty() |
||||
icon.AppendActivityStreamsImage(image) |
||||
service.SetActivityStreamsIcon(icon) |
||||
|
||||
return service |
||||
} |
||||
|
||||
func TestMain(m *testing.M) { |
||||
dbFile, err := ioutil.TempFile(os.TempDir(), "owncast-test-db.db") |
||||
if err != nil { |
||||
panic(err) |
||||
} |
||||
|
||||
data.SetupPersistence(dbFile.Name()) |
||||
data.SetServerURL("https://my.cool.site.biz") |
||||
|
||||
m.Run() |
||||
} |
||||
|
||||
func TestMakeActorFromService(t *testing.T) { |
||||
service := makeFakeService() |
||||
actor := MakeActorFromService(service) |
||||
|
||||
if actor.ActorIri != service.GetJSONLDId().GetIRI() { |
||||
t.Errorf("actor.ID = %v, want %v", actor.ActorIri, service.GetJSONLDId().GetIRI()) |
||||
} |
||||
|
||||
if actor.Name != service.GetActivityStreamsName().At(0).GetXMLSchemaString() { |
||||
t.Errorf("actor.Name = %v, want %v", actor.Name, service.GetActivityStreamsName().At(0).GetXMLSchemaString()) |
||||
} |
||||
|
||||
if actor.Username != service.GetActivityStreamsPreferredUsername().GetXMLSchemaString() { |
||||
t.Errorf("actor.Username = %v, want %v", actor.Username, service.GetActivityStreamsPreferredUsername().GetXMLSchemaString()) |
||||
} |
||||
|
||||
if actor.Inbox != service.GetActivityStreamsInbox().GetIRI() { |
||||
t.Errorf("actor.Inbox = %v, want %v", actor.Inbox.String(), service.GetActivityStreamsInbox().GetIRI()) |
||||
} |
||||
|
||||
if actor.Image != service.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().At(0).GetIRI() { |
||||
t.Errorf("actor.Image = %v, want %v", actor.Image, service.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().At(0).GetIRI()) |
||||
} |
||||
} |
||||
|
||||
func TestMakeActorPropertyWithID(t *testing.T) { |
||||
iri, _ := url.Parse("https://fake.fediverse.server/user/mrfoo") |
||||
actor := MakeActorPropertyWithID(iri) |
||||
|
||||
if actor.Begin().GetIRI() != iri { |
||||
t.Errorf("actor.IRI = %v, want %v", actor.Begin().GetIRI(), iri) |
||||
} |
||||
} |
||||
|
||||
func TestGetFullUsernameFromPerson(t *testing.T) { |
||||
expected := "foodawg@fake.fediverse.server" |
||||
person := makeFakeService() |
||||
username := GetFullUsernameFromService(person) |
||||
|
||||
if username != expected { |
||||
t.Errorf("actor.Username = %v, want %v", username, expected) |
||||
} |
||||
} |
||||
|
||||
func TestAddMetadataLinkToProfile(t *testing.T) { |
||||
person := makeFakeService() |
||||
addMetadataLinkToProfile(person, "my site", "https://my.cool.site.biz") |
||||
attchment := person.GetActivityStreamsAttachment().At(0) |
||||
|
||||
nameValue := attchment.GetActivityStreamsObject().GetActivityStreamsName().At(0).GetXMLSchemaString() |
||||
expected := "my site" |
||||
if nameValue != expected { |
||||
t.Errorf("attachment name = %v, want %v", nameValue, expected) |
||||
} |
||||
|
||||
propertyValue := attchment.GetActivityStreamsObject().GetUnknownProperties()["value"] |
||||
expected = `<a href="https://my.cool.site.biz" rel="me nofollow noopener noreferrer" target="_blank">https://my.cool.site.biz</a>` |
||||
if propertyValue != expected { |
||||
t.Errorf("attachment value = %v, want %v", propertyValue, expected) |
||||
} |
||||
} |
||||
|
||||
func TestMakeServiceForAccount(t *testing.T) { |
||||
person := MakeServiceForAccount("accountname") |
||||
expectedIRI := "https://my.cool.site.biz/federation/user/accountname" |
||||
if person.GetJSONLDId().Get().String() != expectedIRI { |
||||
t.Errorf("actor.IRI = %v, want %v", person.GetJSONLDId().Get().String(), expectedIRI) |
||||
} |
||||
|
||||
if person.GetActivityStreamsPreferredUsername().GetXMLSchemaString() != "accountname" { |
||||
t.Errorf("actor.PreferredUsername = %v, want %v", person.GetActivityStreamsPreferredUsername().GetXMLSchemaString(), expectedIRI) |
||||
} |
||||
|
||||
expectedInbox := "https://my.cool.site.biz/federation/user/accountname/inbox" |
||||
if person.GetActivityStreamsInbox().GetIRI().String() != expectedInbox { |
||||
t.Errorf("actor.Inbox = %v, want %v", person.GetActivityStreamsInbox().GetIRI().String(), expectedInbox) |
||||
} |
||||
|
||||
expectedOutbox := "https://my.cool.site.biz/federation/user/accountname/outbox" |
||||
if person.GetActivityStreamsOutbox().GetIRI().String() != expectedOutbox { |
||||
t.Errorf("actor.Outbox = %v, want %v", person.GetActivityStreamsOutbox().GetIRI().String(), expectedOutbox) |
||||
} |
||||
|
||||
expectedFollowers := "https://my.cool.site.biz/federation/user/accountname/followers" |
||||
if person.GetActivityStreamsFollowers().GetIRI().String() != expectedFollowers { |
||||
t.Errorf("actor.Followers = %v, want %v", person.GetActivityStreamsFollowers().GetIRI().String(), expectedFollowers) |
||||
} |
||||
|
||||
expectedName := "Owncast" |
||||
if person.GetActivityStreamsName().Begin().GetXMLSchemaString() != expectedName { |
||||
t.Errorf("actor.Name = %v, want %v", person.GetActivityStreamsName().Begin().GetXMLSchemaString(), expectedName) |
||||
} |
||||
|
||||
expectedAvatar := "https://my.cool.site.biz/logo/external" |
||||
if person.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI().String() != expectedAvatar { |
||||
t.Errorf("actor.Avatar = %v, want %v", person.GetActivityStreamsIcon().At(0).GetActivityStreamsImage().GetActivityStreamsUrl().Begin().GetIRI().String(), expectedAvatar) |
||||
} |
||||
|
||||
expectedSummary := "Welcome to your new Owncast server! This description can be changed in the admin. Visit https://owncast.online/docs/configuration/ to learn more." |
||||
if person.GetActivityStreamsSummary().At(0).GetXMLSchemaString() != expectedSummary { |
||||
t.Errorf("actor.Summary = %v, want %v", person.GetActivityStreamsSummary().At(0).GetXMLSchemaString(), expectedSummary) |
||||
} |
||||
|
||||
if person.GetActivityStreamsUrl().At(0).GetIRI().String() != expectedIRI { |
||||
t.Errorf("actor.URL = %v, want %v", person.GetActivityStreamsUrl().At(0).GetIRI().String(), expectedIRI) |
||||
} |
||||
} |
@ -0,0 +1,24 @@
@@ -0,0 +1,24 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"net/url" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
) |
||||
|
||||
// MakeHashtag will create and return a mastodon toot hashtag object with the provided name.
|
||||
func MakeHashtag(name string) vocab.TootHashtag { |
||||
u, _ := url.Parse("https://directory.owncast.online/tags/" + name) |
||||
|
||||
hashtag := streams.NewTootHashtag() |
||||
hashtagName := streams.NewActivityStreamsNameProperty() |
||||
hashtagName.AppendXMLSchemaString("#" + name) |
||||
hashtag.SetActivityStreamsName(hashtagName) |
||||
|
||||
hashtagHref := streams.NewActivityStreamsHrefProperty() |
||||
hashtagHref.Set(u) |
||||
hashtag.SetActivityStreamsHref(hashtagHref) |
||||
|
||||
return hashtag |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
package apmodels |
||||
|
||||
import "net/http" |
||||
|
||||
// InboxRequest represents an inbound request to the ActivityPub inbox.
|
||||
type InboxRequest struct { |
||||
Request *http.Request |
||||
ForLocalAccount string |
||||
Body []byte |
||||
} |
@ -0,0 +1,50 @@
@@ -0,0 +1,50 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"net/url" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
) |
||||
|
||||
// CreateCreateActivity will create a new Create Activity model with the provided ID and IRI.
|
||||
func CreateCreateActivity(id string, localAccountIRI *url.URL) vocab.ActivityStreamsCreate { |
||||
objectID := MakeLocalIRIForResource(id) |
||||
message := MakeCreateActivity(objectID) |
||||
|
||||
actorProp := streams.NewActivityStreamsActorProperty() |
||||
actorProp.AppendIRI(localAccountIRI) |
||||
message.SetActivityStreamsActor(actorProp) |
||||
|
||||
return message |
||||
} |
||||
|
||||
// AddImageAttachmentToNote will add the provided image URL to the provided note object.
|
||||
func AddImageAttachmentToNote(note vocab.ActivityStreamsNote, image string) { |
||||
imageURL, err := url.Parse(image) |
||||
if err != nil { |
||||
return |
||||
} |
||||
|
||||
attachments := note.GetActivityStreamsAttachment() |
||||
if attachments == nil { |
||||
attachments = streams.NewActivityStreamsAttachmentProperty() |
||||
} |
||||
|
||||
urlProp := streams.NewActivityStreamsUrlProperty() |
||||
urlProp.AppendIRI(imageURL) |
||||
|
||||
apImage := streams.NewActivityStreamsImage() |
||||
apImage.SetActivityStreamsUrl(urlProp) |
||||
|
||||
imageProp := streams.NewActivityStreamsImageProperty() |
||||
imageProp.AppendActivityStreamsImage(apImage) |
||||
|
||||
imageDescription := streams.NewActivityStreamsContentProperty() |
||||
imageDescription.AppendXMLSchemaString("Live stream preview") |
||||
apImage.SetActivityStreamsContent(imageDescription) |
||||
|
||||
attachments.AppendActivityStreamsImage(apImage) |
||||
|
||||
note.SetActivityStreamsAttachment(attachments) |
||||
} |
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/url" |
||||
"path" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/core/data" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// MakeRemoteIRIForResource will create an IRI for a remote location.
|
||||
func MakeRemoteIRIForResource(resourcePath string, host string) (*url.URL, error) { |
||||
generatedURL := "https://" + host |
||||
u, err := url.Parse(generatedURL) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
u.Path = path.Join(u.Path, "federation", resourcePath) |
||||
|
||||
return u, nil |
||||
} |
||||
|
||||
// MakeLocalIRIForResource will create an IRI for the local server.
|
||||
func MakeLocalIRIForResource(resourcePath string) *url.URL { |
||||
host := data.GetServerURL() |
||||
u, err := url.Parse(host) |
||||
if err != nil { |
||||
log.Errorln("unable to parse local IRI url", host, err) |
||||
return nil |
||||
} |
||||
|
||||
u.Path = path.Join(u.Path, "federation", resourcePath) |
||||
|
||||
return u |
||||
} |
||||
|
||||
// MakeLocalIRIForAccount will return a full IRI for the local server account username.
|
||||
func MakeLocalIRIForAccount(account string) *url.URL { |
||||
host := data.GetServerURL() |
||||
u, err := url.Parse(host) |
||||
if err != nil { |
||||
log.Errorln("unable to parse local IRI account server url", err) |
||||
return nil |
||||
} |
||||
|
||||
u.Path = path.Join(u.Path, "federation", "user", account) |
||||
|
||||
return u |
||||
} |
||||
|
||||
// Serialize will serialize an ActivityPub object to a byte slice.
|
||||
func Serialize(obj vocab.Type) ([]byte, error) { |
||||
var jsonmap map[string]interface{} |
||||
jsonmap, _ = streams.Serialize(obj) |
||||
b, err := json.Marshal(jsonmap) |
||||
|
||||
return b, err |
||||
} |
@ -0,0 +1,43 @@
@@ -0,0 +1,43 @@
|
||||
package apmodels |
||||
|
||||
import ( |
||||
"fmt" |
||||
) |
||||
|
||||
// WebfingerResponse represents a Webfinger response.
|
||||
type WebfingerResponse struct { |
||||
Aliases []string `json:"aliases"` |
||||
Subject string `json:"subject"` |
||||
Links []Link `json:"links"` |
||||
} |
||||
|
||||
// Link represents a Webfinger response Link entity.
|
||||
type Link struct { |
||||
Rel string `json:"rel"` |
||||
Type string `json:"type"` |
||||
Href string `json:"href"` |
||||
} |
||||
|
||||
// MakeWebfingerResponse will create a new Webfinger response.
|
||||
func MakeWebfingerResponse(account string, inbox string, host string) WebfingerResponse { |
||||
accountIRI := MakeLocalIRIForAccount(account) |
||||
|
||||
return WebfingerResponse{ |
||||
Subject: fmt.Sprintf("acct:%s@%s", account, host), |
||||
Aliases: []string{ |
||||
accountIRI.String(), |
||||
}, |
||||
Links: []Link{ |
||||
{ |
||||
Rel: "self", |
||||
Type: "application/activity+json", |
||||
Href: accountIRI.String(), |
||||
}, |
||||
{ |
||||
Rel: "http://webfinger.net/rel/profile-page", |
||||
Type: "text/html", |
||||
Href: accountIRI.String(), |
||||
}, |
||||
}, |
||||
} |
||||
} |
@ -0,0 +1,58 @@
@@ -0,0 +1,58 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strings" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
|
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
// ActorHandler handles requests for a single actor.
|
||||
func ActorHandler(w http.ResponseWriter, r *http.Request) { |
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
pathComponents := strings.Split(r.URL.Path, "/") |
||||
accountName := pathComponents[3] |
||||
|
||||
if _, valid := data.GetFederatedInboxMap()[accountName]; !valid { |
||||
// User is not valid
|
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
// If this request is for an actor's inbox then pass
|
||||
// the request to the inbox controller.
|
||||
if len(pathComponents) == 5 && pathComponents[4] == "inbox" { |
||||
InboxHandler(w, r) |
||||
return |
||||
} else if len(pathComponents) == 5 && pathComponents[4] == "outbox" { |
||||
OutboxHandler(w, r) |
||||
return |
||||
} else if len(pathComponents) == 5 && pathComponents[4] == "followers" { |
||||
// followers list
|
||||
FollowersHandler(w, r) |
||||
return |
||||
} else if len(pathComponents) == 5 && pathComponents[4] == "following" { |
||||
// following list (none)
|
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName) |
||||
publicKey := crypto.GetPublicKey(actorIRI) |
||||
person := apmodels.MakeServiceForAccount(accountName) |
||||
|
||||
if err := requests.WriteStreamResponse(person, w, publicKey); err != nil { |
||||
log.Errorln("unable to write stream response for actor handler", err) |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
} |
@ -0,0 +1,166 @@
@@ -0,0 +1,166 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/pkg/errors" |
||||
log "github.com/sirupsen/logrus" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
const ( |
||||
followersPageSize = 50 |
||||
) |
||||
|
||||
// FollowersHandler will return the list of remote followers on the Fediverse.
|
||||
func FollowersHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != "GET" { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
var response interface{} |
||||
var err error |
||||
if r.URL.Query().Get("page") != "" { |
||||
response, err = getFollowersPage(r.URL.Query().Get("page"), r) |
||||
} else { |
||||
response, err = getInitialFollowersRequest(r) |
||||
} |
||||
|
||||
if response == nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
if err != nil { |
||||
_, _ = w.Write([]byte(err.Error())) |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
pathComponents := strings.Split(r.URL.Path, "/") |
||||
accountName := pathComponents[3] |
||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName) |
||||
publicKey := crypto.GetPublicKey(actorIRI) |
||||
|
||||
if err := requests.WriteStreamResponse(response.(vocab.Type), w, publicKey); err != nil { |
||||
log.Errorln("unable to write stream response for followers handler", err) |
||||
} |
||||
} |
||||
|
||||
func getInitialFollowersRequest(r *http.Request) (vocab.ActivityStreamsOrderedCollection, error) { |
||||
followerCount, _ := persistence.GetFollowerCount() |
||||
collection := streams.NewActivityStreamsOrderedCollection() |
||||
idProperty := streams.NewJSONLDIdProperty() |
||||
id, err := createPageURL(r, nil) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create followers page property") |
||||
} |
||||
idProperty.SetIRI(id) |
||||
collection.SetJSONLDId(idProperty) |
||||
|
||||
totalItemsProperty := streams.NewActivityStreamsTotalItemsProperty() |
||||
totalItemsProperty.Set(int(followerCount)) |
||||
collection.SetActivityStreamsTotalItems(totalItemsProperty) |
||||
|
||||
first := streams.NewActivityStreamsFirstProperty() |
||||
page := "1" |
||||
firstIRI, err := createPageURL(r, &page) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create first page property") |
||||
} |
||||
|
||||
first.SetIRI(firstIRI) |
||||
collection.SetActivityStreamsFirst(first) |
||||
|
||||
return collection, nil |
||||
} |
||||
|
||||
func getFollowersPage(page string, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { |
||||
pageInt, err := strconv.Atoi(page) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse page number") |
||||
} |
||||
|
||||
followerCount, err := persistence.GetFollowerCount() |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to get follower count") |
||||
} |
||||
|
||||
followers, err := persistence.GetFederationFollowers(followersPageSize, (pageInt-1)*followersPageSize) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to get federation followers") |
||||
} |
||||
|
||||
collectionPage := streams.NewActivityStreamsOrderedCollectionPage() |
||||
idProperty := streams.NewJSONLDIdProperty() |
||||
id, err := createPageURL(r, &page) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create followers page ID") |
||||
} |
||||
idProperty.SetIRI(id) |
||||
collectionPage.SetJSONLDId(idProperty) |
||||
|
||||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty() |
||||
|
||||
for _, follower := range followers { |
||||
u, _ := url.Parse(follower.ActorIRI) |
||||
orderedItems.AppendIRI(u) |
||||
} |
||||
collectionPage.SetActivityStreamsOrderedItems(orderedItems) |
||||
|
||||
partOf := streams.NewActivityStreamsPartOfProperty() |
||||
partOfIRI, err := createPageURL(r, nil) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create partOf property for followers page") |
||||
} |
||||
|
||||
partOf.SetIRI(partOfIRI) |
||||
collectionPage.SetActivityStreamsPartOf(partOf) |
||||
|
||||
if pageInt*followersPageSize < int(followerCount) { |
||||
next := streams.NewActivityStreamsNextProperty() |
||||
nextPage := fmt.Sprintf("%d", pageInt+1) |
||||
nextIRI, err := createPageURL(r, &nextPage) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create next page property") |
||||
} |
||||
|
||||
next.SetIRI(nextIRI) |
||||
collectionPage.SetActivityStreamsNext(next) |
||||
} |
||||
|
||||
return collectionPage, nil |
||||
} |
||||
|
||||
func createPageURL(r *http.Request, page *string) (*url.URL, error) { |
||||
domain := data.GetServerURL() |
||||
if domain == "" { |
||||
return nil, errors.New("unable to get server URL") |
||||
} |
||||
|
||||
pageURL, err := url.Parse(domain) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse server URL") |
||||
} |
||||
|
||||
if page != nil { |
||||
query := pageURL.Query() |
||||
query.Add("page", *page) |
||||
pageURL.RawQuery = query.Encode() |
||||
} |
||||
pageURL.Path = r.URL.Path |
||||
|
||||
return pageURL, nil |
||||
} |
@ -0,0 +1,56 @@
@@ -0,0 +1,56 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"io" |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/inbox" |
||||
"github.com/owncast/owncast/core/data" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// InboxHandler handles inbound federated requests.
|
||||
func InboxHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method == http.MethodPost { |
||||
acceptInboxRequest(w, r) |
||||
} else { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
} |
||||
} |
||||
|
||||
func acceptInboxRequest(w http.ResponseWriter, r *http.Request) { |
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
urlPathComponents := strings.Split(r.URL.Path, "/") |
||||
var forLocalAccount string |
||||
if len(urlPathComponents) == 5 { |
||||
forLocalAccount = urlPathComponents[3] |
||||
} else { |
||||
log.Errorln("Unable to determine username from url path") |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
// The account this request is for must match the account name we have set
|
||||
// for federation.
|
||||
if forLocalAccount != data.GetFederationUsername() { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
data, err := io.ReadAll(r.Body) |
||||
if err != nil { |
||||
log.Errorln("Unable to read inbox request payload", err) |
||||
return |
||||
} |
||||
|
||||
inboxRequest := apmodels.InboxRequest{Request: r, ForLocalAccount: forLocalAccount, Body: data} |
||||
inbox.AddToQueue(inboxRequest) |
||||
w.WriteHeader(http.StatusAccepted) |
||||
} |
@ -0,0 +1,285 @@
@@ -0,0 +1,285 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
|
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/config" |
||||
"github.com/owncast/owncast/core/data" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// NodeInfoController returns the V1 node info response.
|
||||
func NodeInfoController(w http.ResponseWriter, r *http.Request) { |
||||
type links struct { |
||||
Rel string `json:"rel"` |
||||
Href string `json:"href"` |
||||
} |
||||
|
||||
type response struct { |
||||
Links []links `json:"links"` |
||||
} |
||||
|
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
serverURL := data.GetServerURL() |
||||
if serverURL == "" { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
v2, err := url.Parse(serverURL) |
||||
if err != nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
v2.Path = "nodeinfo/2.0" |
||||
|
||||
res := response{ |
||||
Links: []links{ |
||||
{ |
||||
Rel: "http://nodeinfo.diaspora.software/ns/schema/2.0", |
||||
Href: v2.String(), |
||||
}, |
||||
}, |
||||
} |
||||
if err := writeResponse(res, w); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
||||
|
||||
// NodeInfoV2Controller returns the V2 node info response.
|
||||
func NodeInfoV2Controller(w http.ResponseWriter, r *http.Request) { |
||||
type software struct { |
||||
Name string `json:"name"` |
||||
Version string `json:"version"` |
||||
} |
||||
type users struct { |
||||
Total int `json:"total"` |
||||
ActiveMonth int `json:"activeMonth"` |
||||
ActiveHalfyear int `json:"activeHalfyear"` |
||||
} |
||||
type usage struct { |
||||
Users users `json:"users"` |
||||
LocalPosts int `json:"localPosts"` |
||||
} |
||||
type response struct { |
||||
Version string `json:"version"` |
||||
Software software `json:"software"` |
||||
Protocols []string `json:"protocols"` |
||||
Usage usage `json:"usage"` |
||||
OpenRegistrations bool `json:"openRegistrations"` |
||||
} |
||||
|
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
localPostCount, _ := persistence.GetLocalPostCount() |
||||
|
||||
res := response{ |
||||
Version: "2.0", |
||||
Software: software{ |
||||
Name: "Owncast", |
||||
Version: config.VersionNumber, |
||||
}, |
||||
Usage: usage{ |
||||
Users: users{ |
||||
Total: 1, |
||||
ActiveMonth: 1, |
||||
ActiveHalfyear: 1, |
||||
}, |
||||
LocalPosts: int(localPostCount), |
||||
}, |
||||
OpenRegistrations: false, |
||||
Protocols: []string{"activitypub"}, |
||||
} |
||||
|
||||
if err := writeResponse(res, w); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
||||
|
||||
// XNodeInfo2Controller returns the x-nodeinfo2.
|
||||
func XNodeInfo2Controller(w http.ResponseWriter, r *http.Request) { |
||||
type Organization struct { |
||||
Name string `json:"name"` |
||||
Contact string `json:"contact"` |
||||
} |
||||
type Server struct { |
||||
BaseURL string `json:"baseUrl"` |
||||
Version string `json:"version"` |
||||
Name string `json:"name"` |
||||
Software string `json:"software"` |
||||
} |
||||
type Services struct { |
||||
Outbound []string `json:"outbound"` |
||||
Inbound []string `json:"inbound"` |
||||
} |
||||
type Users struct { |
||||
ActiveWeek int `json:"activeWeek"` |
||||
Total int `json:"total"` |
||||
ActiveMonth int `json:"activeMonth"` |
||||
ActiveHalfyear int `json:"activeHalfyear"` |
||||
} |
||||
type Usage struct { |
||||
Users Users `json:"users"` |
||||
LocalPosts int `json:"localPosts"` |
||||
LocalComments int `json:"localComments"` |
||||
} |
||||
type response struct { |
||||
Organization Organization `json:"organization"` |
||||
Server Server `json:"server"` |
||||
Services Services `json:"services"` |
||||
Protocols []string `json:"protocols"` |
||||
Version string `json:"version"` |
||||
OpenRegistrations bool `json:"openRegistrations"` |
||||
Usage Usage `json:"usage"` |
||||
} |
||||
|
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
serverURL := data.GetServerURL() |
||||
if serverURL == "" { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
localPostCount, _ := persistence.GetLocalPostCount() |
||||
|
||||
res := &response{ |
||||
Organization: Organization{ |
||||
Name: data.GetServerName(), |
||||
Contact: serverURL, |
||||
}, |
||||
Server: Server{ |
||||
BaseURL: serverURL, |
||||
Version: config.VersionNumber, |
||||
Name: "owncast", |
||||
Software: "owncast", |
||||
}, |
||||
Services: Services{ |
||||
Inbound: []string{"activitypub"}, |
||||
Outbound: []string{"activitypub"}, |
||||
}, |
||||
Protocols: []string{"activitypub"}, |
||||
Version: config.VersionNumber, |
||||
Usage: Usage{ |
||||
Users: Users{ |
||||
ActiveWeek: 1, |
||||
Total: 1, |
||||
ActiveMonth: 1, |
||||
ActiveHalfyear: 1, |
||||
}, |
||||
|
||||
LocalPosts: int(localPostCount), |
||||
LocalComments: 0, |
||||
}, |
||||
} |
||||
|
||||
if err := writeResponse(res, w); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
||||
|
||||
// InstanceV1Controller returns the v1 instance details.
|
||||
func InstanceV1Controller(w http.ResponseWriter, r *http.Request) { |
||||
type Stats struct { |
||||
UserCount int `json:"user_count"` |
||||
StatusCount int `json:"status_count"` |
||||
DomainCount int `json:"domain_count"` |
||||
} |
||||
type response struct { |
||||
URI string `json:"uri"` |
||||
Title string `json:"title"` |
||||
ShortDescription string `json:"short_description"` |
||||
Description string `json:"description"` |
||||
Version string `json:"version"` |
||||
Stats Stats `json:"stats"` |
||||
Thumbnail string `json:"thumbnail"` |
||||
Languages []string `json:"languages"` |
||||
Registrations bool `json:"registrations"` |
||||
ApprovalRequired bool `json:"approval_required"` |
||||
InvitesEnabled bool `json:"invites_enabled"` |
||||
} |
||||
|
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
serverURL := data.GetServerURL() |
||||
if serverURL == "" { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
thumbnail, err := url.Parse(serverURL) |
||||
if err != nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
thumbnail.Path = "/logo/external" |
||||
localPostCount, _ := persistence.GetLocalPostCount() |
||||
|
||||
res := response{ |
||||
URI: serverURL, |
||||
Title: data.GetServerName(), |
||||
ShortDescription: data.GetServerSummary(), |
||||
Description: data.GetServerSummary(), |
||||
Version: config.GetReleaseString(), |
||||
Stats: Stats{ |
||||
UserCount: 1, |
||||
StatusCount: int(localPostCount), |
||||
DomainCount: 0, |
||||
}, |
||||
Thumbnail: thumbnail.String(), |
||||
Registrations: false, |
||||
ApprovalRequired: false, |
||||
InvitesEnabled: false, |
||||
} |
||||
|
||||
if err := writeResponse(res, w); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
||||
|
||||
func writeResponse(payload interface{}, w http.ResponseWriter) error { |
||||
accountName := data.GetDefaultFederationUsername() |
||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName) |
||||
publicKey := crypto.GetPublicKey(actorIRI) |
||||
|
||||
return requests.WritePayloadResponse(payload, w, publicKey) |
||||
} |
||||
|
||||
// HostMetaController points to webfinger.
|
||||
func HostMetaController(w http.ResponseWriter, r *http.Request) { |
||||
serverURL := data.GetServerURL() |
||||
if serverURL == "" { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
res := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?> |
||||
<XRD xmlns="http://docs.oasis-open.org/ns/xri/xrd-1.0"> |
||||
<Link rel="lrdd" type="application/json" template="%s/.well-known/webfinger?resource={uri}"/> |
||||
</XRD>`, serverURL) |
||||
|
||||
if _, err := w.Write([]byte(res)); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
@ -0,0 +1,42 @@
@@ -0,0 +1,42 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/core/data" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// ObjectHandler handles requests for a single federated ActivityPub object.
|
||||
func ObjectHandler(w http.ResponseWriter, r *http.Request) { |
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
// If private federation mode is enabled do not allow access to objects.
|
||||
if data.GetFederationIsPrivate() { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
iri := strings.Join([]string{strings.TrimSuffix(data.GetServerURL(), "/"), r.URL.Path}, "") |
||||
object, _, _, err := persistence.GetObjectByIRI(iri) |
||||
if err != nil { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
accountName := data.GetDefaultFederationUsername() |
||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName) |
||||
publicKey := crypto.GetPublicKey(actorIRI) |
||||
|
||||
if err := requests.WriteResponse([]byte(object), w, publicKey); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
@ -0,0 +1,156 @@
@@ -0,0 +1,156 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"fmt" |
||||
"net/http" |
||||
"strconv" |
||||
"strings" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/pkg/errors" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
const ( |
||||
outboxPageSize = 50 |
||||
) |
||||
|
||||
// OutboxHandler will handle requests for the local ActivityPub outbox.
|
||||
func OutboxHandler(w http.ResponseWriter, r *http.Request) { |
||||
if r.Method != http.MethodGet { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
var response interface{} |
||||
var err error |
||||
if r.URL.Query().Get("page") != "" { |
||||
response, err = getOutboxPage(r.URL.Query().Get("page"), r) |
||||
} else { |
||||
response, err = getInitialOutboxHandler(r) |
||||
} |
||||
|
||||
if response == nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
if err != nil { |
||||
_, _ = w.Write([]byte(err.Error())) |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return |
||||
} |
||||
|
||||
pathComponents := strings.Split(r.URL.Path, "/") |
||||
accountName := pathComponents[3] |
||||
actorIRI := apmodels.MakeLocalIRIForAccount(accountName) |
||||
publicKey := crypto.GetPublicKey(actorIRI) |
||||
|
||||
if err := requests.WriteStreamResponse(response.(vocab.Type), w, publicKey); err != nil { |
||||
log.Errorln("unable to write stream response for outbox handler", err) |
||||
} |
||||
} |
||||
|
||||
// ActorObjectHandler will handle the request for a single ActivityPub object.
|
||||
func ActorObjectHandler(w http.ResponseWriter, r *http.Request) { |
||||
object, _, _, err := persistence.GetObjectByIRI(r.URL.Path) |
||||
if err != nil { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
// controllers.WriteSimpleResponse(w, false, err.Error())
|
||||
} |
||||
|
||||
if _, err := w.Write([]byte(object)); err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
} |
||||
|
||||
func getInitialOutboxHandler(r *http.Request) (vocab.ActivityStreamsOrderedCollection, error) { |
||||
collection := streams.NewActivityStreamsOrderedCollection() |
||||
|
||||
idProperty := streams.NewJSONLDIdProperty() |
||||
id, err := createPageURL(r, nil) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create followers page property") |
||||
} |
||||
idProperty.SetIRI(id) |
||||
collection.SetJSONLDId(idProperty) |
||||
|
||||
totalPosts, err := persistence.GetOutboxPostCount() |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to get outbox post count") |
||||
} |
||||
totalItemsProperty := streams.NewActivityStreamsTotalItemsProperty() |
||||
totalItemsProperty.Set(int(totalPosts)) |
||||
collection.SetActivityStreamsTotalItems(totalItemsProperty) |
||||
|
||||
first := streams.NewActivityStreamsFirstProperty() |
||||
page := "1" |
||||
firstIRI, err := createPageURL(r, &page) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create first page property") |
||||
} |
||||
|
||||
first.SetIRI(firstIRI) |
||||
collection.SetActivityStreamsFirst(first) |
||||
|
||||
return collection, nil |
||||
} |
||||
|
||||
func getOutboxPage(page string, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { |
||||
pageInt, err := strconv.Atoi(page) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to parse page number") |
||||
} |
||||
|
||||
postCount, err := persistence.GetOutboxPostCount() |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to get outbox post count") |
||||
} |
||||
|
||||
collectionPage := streams.NewActivityStreamsOrderedCollectionPage() |
||||
idProperty := streams.NewJSONLDIdProperty() |
||||
id, err := createPageURL(r, &page) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create followers page ID") |
||||
} |
||||
idProperty.SetIRI(id) |
||||
collectionPage.SetJSONLDId(idProperty) |
||||
|
||||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty() |
||||
|
||||
outboxItems, err := persistence.GetOutbox(outboxPageSize, (pageInt-1)*outboxPageSize) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to get federation followers") |
||||
} |
||||
orderedItems.AppendActivityStreamsOrderedCollection(outboxItems) |
||||
collectionPage.SetActivityStreamsOrderedItems(orderedItems) |
||||
|
||||
partOf := streams.NewActivityStreamsPartOfProperty() |
||||
partOfIRI, err := createPageURL(r, nil) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create partOf property for outbox page") |
||||
} |
||||
|
||||
partOf.SetIRI(partOfIRI) |
||||
collectionPage.SetActivityStreamsPartOf(partOf) |
||||
|
||||
if pageInt*followersPageSize < int(postCount) { |
||||
next := streams.NewActivityStreamsNextProperty() |
||||
nextPage := fmt.Sprintf("%d", pageInt+1) |
||||
nextIRI, err := createPageURL(r, &nextPage) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "unable to create next page property") |
||||
} |
||||
|
||||
next.SetIRI(nextIRI) |
||||
collectionPage.SetActivityStreamsNext(next) |
||||
} |
||||
|
||||
return collectionPage, nil |
||||
} |
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/utils" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// WebfingerHandler will handle webfinger lookup requests.
|
||||
func WebfingerHandler(w http.ResponseWriter, r *http.Request) { |
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
resource := r.URL.Query().Get("resource") |
||||
resourceComponents := strings.Split(resource, ":") |
||||
account := resourceComponents[1] |
||||
|
||||
userComponents := strings.Split(account, "@") |
||||
if len(userComponents) < 2 { |
||||
return |
||||
} |
||||
host := userComponents[1] |
||||
user := userComponents[0] |
||||
|
||||
if _, valid := data.GetFederatedInboxMap()[user]; !valid { |
||||
// User is not valid
|
||||
w.WriteHeader(http.StatusNotFound) |
||||
log.Println("Webfinger request rejected") |
||||
return |
||||
} |
||||
|
||||
// If the webfinger request doesn't match our server then it
|
||||
// should be rejected.
|
||||
instanceHostString := data.GetServerURL() |
||||
if instanceHostString == "" { |
||||
w.WriteHeader(http.StatusNotImplemented) |
||||
return |
||||
} |
||||
|
||||
instanceHostString = utils.GetHostnameFromURLString(instanceHostString) |
||||
if instanceHostString == "" || instanceHostString != host { |
||||
w.WriteHeader(http.StatusNotImplemented) |
||||
return |
||||
} |
||||
|
||||
webfingerResponse := apmodels.MakeWebfingerResponse(user, user, host) |
||||
|
||||
w.Header().Set("Content-Type", "application/jrd+json") |
||||
|
||||
if err := json.NewEncoder(w).Encode(webfingerResponse); err != nil { |
||||
log.Errorln("unable to write webfinger response", err) |
||||
} |
||||
} |
@ -0,0 +1,78 @@
@@ -0,0 +1,78 @@
|
||||
package crypto |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"crypto/rsa" |
||||
"crypto/x509" |
||||
"encoding/pem" |
||||
"errors" |
||||
"net/url" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// GetPublicKey will return the public key for the provided actor.
|
||||
func GetPublicKey(actorIRI *url.URL) PublicKey { |
||||
key := data.GetPublicKey() |
||||
idURL, err := url.Parse(actorIRI.String() + "#main-key") |
||||
if err != nil { |
||||
log.Errorln("unable to parse actor iri string", idURL, err) |
||||
} |
||||
|
||||
return PublicKey{ |
||||
ID: idURL, |
||||
Owner: actorIRI, |
||||
PublicKeyPem: key, |
||||
} |
||||
} |
||||
|
||||
// GetPrivateKey will return the internal server private key.
|
||||
func GetPrivateKey() *rsa.PrivateKey { |
||||
key := data.GetPrivateKey() |
||||
|
||||
block, _ := pem.Decode([]byte(key)) |
||||
if block == nil { |
||||
log.Errorln(errors.New("failed to parse PEM block containing the key")) |
||||
return nil |
||||
} |
||||
|
||||
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes) |
||||
if err != nil { |
||||
log.Errorln("unable to parse private key", err) |
||||
return nil |
||||
} |
||||
|
||||
return priv |
||||
} |
||||
|
||||
// GenerateKeys will generate the private/public key pair needed for federation.
|
||||
func GenerateKeys() ([]byte, []byte, error) { |
||||
// generate key
|
||||
privatekey, err := rsa.GenerateKey(rand.Reader, 2048) |
||||
if err != nil { |
||||
log.Errorln("Cannot generate RSA key", err) |
||||
return nil, nil, err |
||||
} |
||||
publickey := &privatekey.PublicKey |
||||
|
||||
privateKeyBytes := x509.MarshalPKCS1PrivateKey(privatekey) |
||||
privateKeyBlock := &pem.Block{ |
||||
Type: "RSA PRIVATE KEY", |
||||
Bytes: privateKeyBytes, |
||||
} |
||||
privatePem := pem.EncodeToMemory(privateKeyBlock) |
||||
|
||||
publicKeyBytes, err := x509.MarshalPKIXPublicKey(publickey) |
||||
if err != nil { |
||||
log.Errorln("error when dumping publickey:", err) |
||||
return nil, nil, err |
||||
} |
||||
publicKeyBlock := &pem.Block{ |
||||
Type: "PUBLIC KEY", |
||||
Bytes: publicKeyBytes, |
||||
} |
||||
publicPem := pem.EncodeToMemory(publicKeyBlock) |
||||
|
||||
return privatePem, publicPem, nil |
||||
} |
@ -0,0 +1,10 @@
@@ -0,0 +1,10 @@
|
||||
package crypto |
||||
|
||||
import "net/url" |
||||
|
||||
// PublicKey represents a public key with associated ownership.
|
||||
type PublicKey struct { |
||||
ID *url.URL `json:"id"` |
||||
Owner *url.URL `json:"owner"` |
||||
PublicKeyPem string `json:"publicKeyPem"` |
||||
} |
@ -0,0 +1,70 @@
@@ -0,0 +1,70 @@
|
||||
package crypto |
||||
|
||||
import ( |
||||
"crypto" |
||||
"net/http" |
||||
"net/url" |
||||
"time" |
||||
|
||||
"github.com/go-fed/httpsig" |
||||
) |
||||
|
||||
// SignResponse will sign a response using the provided response body and public key.
|
||||
func SignResponse(w http.ResponseWriter, body []byte, publicKey PublicKey) error { |
||||
privateKey := GetPrivateKey() |
||||
|
||||
return signResponse(privateKey, *publicKey.ID, body, w) |
||||
} |
||||
|
||||
func signResponse(privateKey crypto.PrivateKey, pubKeyID url.URL, body []byte, w http.ResponseWriter) error { |
||||
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256} |
||||
digestAlgorithm := httpsig.DigestSha256 |
||||
|
||||
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
|
||||
headersToSign := []string{} |
||||
if body != nil { |
||||
headersToSign = append(headersToSign, "digest") |
||||
} |
||||
|
||||
signer, _, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature, 0) |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// If r were a http.ResponseWriter, call SignResponse instead.
|
||||
return signer.SignResponse(privateKey, pubKeyID.String(), w, body) |
||||
} |
||||
|
||||
// SignRequest will sign an ounbound request given the provided body.
|
||||
func SignRequest(req *http.Request, body []byte, actorIRI *url.URL) error { |
||||
publicKey := GetPublicKey(actorIRI) |
||||
privateKey := GetPrivateKey() |
||||
|
||||
return signRequest(privateKey, publicKey.ID.String(), body, req) |
||||
} |
||||
|
||||
func signRequest(privateKey crypto.PrivateKey, pubKeyID string, body []byte, r *http.Request) error { |
||||
prefs := []httpsig.Algorithm{httpsig.RSA_SHA256} |
||||
digestAlgorithm := httpsig.DigestSha256 |
||||
|
||||
date := time.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05 GMT") |
||||
r.Header["Date"] = []string{date} |
||||
r.Header["Host"] = []string{r.URL.Host} |
||||
r.Header["Accept"] = []string{`application/ld+json; profile="https://www.w3.org/ns/activitystreams"`} |
||||
|
||||
// The "Date" and "Digest" headers must already be set on r, as well as r.URL.
|
||||
headersToSign := []string{httpsig.RequestTarget, "host", "date"} |
||||
if body != nil { |
||||
headersToSign = append(headersToSign, "digest") |
||||
} |
||||
|
||||
signer, _, err := httpsig.NewSigner(prefs, digestAlgorithm, headersToSign, httpsig.Signature, 0) |
||||
|
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// If r were a http.ResponseWriter, call SignResponse instead.
|
||||
return signer.SignRequest(privateKey, pubKeyID, r, body) |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
func handleAnnounceRequest(c context.Context, activity vocab.ActivityStreamsAnnounce) error { |
||||
object := activity.GetActivityStreamsObject() |
||||
actorReference := activity.GetActivityStreamsActor() |
||||
objectIRI := object.At(0).GetIRI().String() |
||||
actorIRI := actorReference.At(0).GetIRI().String() |
||||
|
||||
if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementRepost); hasPreviouslyhandled || err != nil { |
||||
return errors.Wrap(err, "inbound activity of share/re-post has already been handled") |
||||
} |
||||
|
||||
// Shares need to match a post we had already sent.
|
||||
_, isLiveNotification, timestamp, err := persistence.GetObjectByIRI(objectIRI) |
||||
if err != nil { |
||||
return errors.Wrap(err, "Could not find post locally") |
||||
} |
||||
|
||||
// Don't allow old activities to be liked
|
||||
if time.Since(timestamp) > maxAgeForEngagement { |
||||
return errors.New("Activity is too old to be shared") |
||||
} |
||||
|
||||
// Save as an accepted activity
|
||||
if err := persistence.SaveInboundFediverseActivity(objectIRI, actorIRI, events.FediverseEngagementRepost, time.Now()); err != nil { |
||||
return errors.Wrap(err, "unable to save inbound share/re-post activity") |
||||
} |
||||
|
||||
return handleEngagementActivity(events.FediverseEngagementRepost, isLiveNotification, actorReference, events.FediverseEngagementRepost) |
||||
} |
@ -0,0 +1,62 @@
@@ -0,0 +1,62 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/resolvers" |
||||
"github.com/owncast/owncast/core/chat" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
func handleEngagementActivity(eventType events.EventType, isLiveNotification bool, actorReference vocab.ActivityStreamsActorProperty, action string) error { |
||||
// Do nothing if displaying engagement actions has been turned off.
|
||||
if !data.GetFederationShowEngagement() { |
||||
return nil |
||||
} |
||||
|
||||
// Do nothing if chat is disabled
|
||||
if data.GetChatDisabled() { |
||||
return nil |
||||
} |
||||
|
||||
// Get actor of the action
|
||||
actor, _ := resolvers.GetResolvedActorFromActorProperty(actorReference) |
||||
|
||||
// Send chat message
|
||||
actorName := actor.Name |
||||
if actorName == "" { |
||||
actorName = actor.Username |
||||
} |
||||
actorIRI := actorReference.Begin().GetIRI().String() |
||||
|
||||
userPrefix := fmt.Sprintf("%s ", actorName) |
||||
var suffix string |
||||
if isLiveNotification && action == events.FediverseEngagementLike { |
||||
suffix = "liked that this stream went live." |
||||
} else if action == events.FediverseEngagementLike { |
||||
suffix = fmt.Sprintf("liked a post from %s.", data.GetServerName()) |
||||
} else if isLiveNotification && action == events.FediverseEngagementRepost { |
||||
suffix = "shared this stream with their followers." |
||||
} else if action == events.FediverseEngagementRepost { |
||||
suffix = fmt.Sprintf("shared a post from %s.", data.GetServerName()) |
||||
} else if action == events.FediverseEngagementFollow { |
||||
suffix = "followed this stream." |
||||
} else { |
||||
return fmt.Errorf("could not handle event for sending to chat: %s", action) |
||||
} |
||||
body := fmt.Sprintf("%s %s", userPrefix, suffix) |
||||
|
||||
var image *string |
||||
if actor.Image != nil { |
||||
s := actor.Image.String() |
||||
image = &s |
||||
} |
||||
|
||||
if err := chat.SendFediverseAction(eventType, actor.FullUsername, image, body, actorIRI); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,7 @@
@@ -0,0 +1,7 @@
|
||||
package inbox |
||||
|
||||
import "time" |
||||
|
||||
const ( |
||||
maxAgeForEngagement = time.Hour * 36 |
||||
) |
@ -0,0 +1,88 @@
@@ -0,0 +1,88 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"context" |
||||
"fmt" |
||||
"time" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/activitypub/resolvers" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/pkg/errors" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsFollow) error { |
||||
follow, err := resolvers.MakeFollowRequest(c, activity) |
||||
if err != nil { |
||||
log.Errorln("unable to create follow inbox request", err) |
||||
return err |
||||
} |
||||
|
||||
if follow == nil { |
||||
return fmt.Errorf("unable to handle request") |
||||
} |
||||
|
||||
approved := !data.GetFederationIsPrivate() |
||||
|
||||
followRequest := *follow |
||||
|
||||
if err := persistence.AddFollow(followRequest, approved); err != nil { |
||||
log.Errorln("unable to save follow request", err) |
||||
return err |
||||
} |
||||
|
||||
localAccountName := data.GetDefaultFederationUsername() |
||||
|
||||
if approved { |
||||
if err := requests.SendFollowAccept(follow.Inbox, follow.FollowRequestIri, localAccountName); err != nil { |
||||
log.Errorln("unable to send follow accept", err) |
||||
return err |
||||
} |
||||
} |
||||
|
||||
// Save as an accepted activity
|
||||
actorReference := activity.GetActivityStreamsActor() |
||||
object := activity.GetActivityStreamsObject() |
||||
objectIRI := object.At(0).GetIRI().String() |
||||
actorIRI := actorReference.At(0).GetIRI().String() |
||||
|
||||
// If this request is approved and we have not previously sent an action to
|
||||
// chat due to a previous follow request, then do so.
|
||||
hasPreviouslyhandled := true // Default so we don't send anything if it fails.
|
||||
if approved { |
||||
hasPreviouslyhandled, err = persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementFollow) |
||||
if err != nil { |
||||
log.Errorln("error checking for previously handled follow activity", err) |
||||
} |
||||
} |
||||
|
||||
// Save this follow action to our activities table.
|
||||
if err := persistence.SaveInboundFediverseActivity(objectIRI, actorIRI, events.FediverseEngagementFollow, time.Now()); err != nil { |
||||
return errors.Wrap(err, "unable to save inbound share/re-post activity") |
||||
} |
||||
|
||||
// Send action to chat if it has not been previously handled.
|
||||
if !hasPreviouslyhandled { |
||||
return handleEngagementActivity(events.FediverseEngagementFollow, false, actorReference, events.FediverseEngagementFollow) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func handleUnfollowRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { |
||||
request := resolvers.MakeUnFollowRequest(c, activity) |
||||
if request == nil { |
||||
log.Errorf("unable to handle unfollow request") |
||||
return errors.New("unable to handle unfollow request") |
||||
} |
||||
|
||||
unfollowRequest := *request |
||||
log.Traceln("unfollow request:", unfollowRequest) |
||||
|
||||
return persistence.RemoveFollow(unfollowRequest) |
||||
} |
@ -0,0 +1,40 @@
@@ -0,0 +1,40 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"context" |
||||
"time" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/pkg/errors" |
||||
) |
||||
|
||||
func handleLikeRequest(c context.Context, activity vocab.ActivityStreamsLike) error { |
||||
object := activity.GetActivityStreamsObject() |
||||
actorReference := activity.GetActivityStreamsActor() |
||||
objectIRI := object.At(0).GetIRI().String() |
||||
actorIRI := actorReference.At(0).GetIRI().String() |
||||
|
||||
if hasPreviouslyhandled, err := persistence.HasPreviouslyHandledInboundActivity(objectIRI, actorIRI, events.FediverseEngagementLike); hasPreviouslyhandled || err != nil { |
||||
return errors.Wrap(err, "inbound activity of like has already been handled") |
||||
} |
||||
|
||||
// Likes need to match a post we had already sent.
|
||||
_, isLiveNotification, timestamp, err := persistence.GetObjectByIRI(objectIRI) |
||||
if err != nil { |
||||
return errors.Wrap(err, "Could not find post locally") |
||||
} |
||||
|
||||
// Don't allow old activities to be liked
|
||||
if time.Since(timestamp) > maxAgeForEngagement { |
||||
return errors.New("Activity is too old to be liked") |
||||
} |
||||
|
||||
// Save as an accepted activity
|
||||
if err := persistence.SaveInboundFediverseActivity(objectIRI, actorIRI, events.FediverseEngagementLike, time.Now()); err != nil { |
||||
return errors.Wrap(err, "unable to save inbound like activity") |
||||
} |
||||
|
||||
return handleEngagementActivity(events.FediverseEngagementLike, isLiveNotification, actorReference, events.FediverseEngagementLike) |
||||
} |
@ -0,0 +1,27 @@
@@ -0,0 +1,27 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
) |
||||
|
||||
func handleUndoInboxRequest(c context.Context, activity vocab.ActivityStreamsUndo) error { |
||||
// Determine if this is an undo of a follow, favorite, announce, etc.
|
||||
o := activity.GetActivityStreamsObject() |
||||
for iter := o.Begin(); iter != o.End(); iter = iter.Next() { |
||||
if iter.IsActivityStreamsFollow() { |
||||
// This is an Unfollow request
|
||||
if err := handleUnfollowRequest(c, activity); err != nil { |
||||
return err |
||||
} |
||||
} else { |
||||
log.Traceln("Undo", iter.GetType().GetTypeName(), "ignored") |
||||
return nil |
||||
} |
||||
} |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,25 @@
@@ -0,0 +1,25 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/resolvers" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func handleUpdateRequest(c context.Context, activity vocab.ActivityStreamsUpdate) error { |
||||
// We only care about update events to followers.
|
||||
if !activity.GetActivityStreamsObject().At(0).IsActivityStreamsPerson() { |
||||
return nil |
||||
} |
||||
|
||||
actor, err := resolvers.GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
return err |
||||
} |
||||
|
||||
return persistence.UpdateFollower(actor.ActorIri.String(), actor.Inbox.String(), actor.Name, actor.FullUsername, actor.Image.String()) |
||||
} |
@ -0,0 +1,130 @@
@@ -0,0 +1,130 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"context" |
||||
"crypto/x509" |
||||
"encoding/pem" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
|
||||
"github.com/pkg/errors" |
||||
|
||||
"github.com/go-fed/httpsig" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/resolvers" |
||||
"github.com/owncast/owncast/core/data" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func handle(request apmodels.InboxRequest) { |
||||
if verified, err := Verify(request.Request); err != nil { |
||||
log.Debugln("Error in attempting to verify request", err) |
||||
return |
||||
} else if !verified { |
||||
log.Errorln("Request failed verification", err) |
||||
return |
||||
} |
||||
|
||||
// c := context.WithValue(context.Background(), "account", request.ForLocalAccount) //nolint
|
||||
|
||||
if err := resolvers.Resolve(context.Background(), request.Body, handleUpdateRequest, handleFollowInboxRequest, handleLikeRequest, handleAnnounceRequest, handleUndoInboxRequest); err != nil { |
||||
log.Errorln("resolver error:", err) |
||||
} |
||||
} |
||||
|
||||
// Verify will Verify the http signature of an inbound request as well as
|
||||
// check it against the list of blocked domains.
|
||||
func Verify(request *http.Request) (bool, error) { |
||||
verifier, err := httpsig.NewVerifier(request) |
||||
if err != nil { |
||||
return false, errors.Wrap(err, "failed to create key verifier for request") |
||||
} |
||||
pubKeyID, err := url.Parse(verifier.KeyId()) |
||||
if err != nil { |
||||
return false, errors.Wrap(err, "failed to parse key to get key ID") |
||||
} |
||||
|
||||
// Force federation only via servers using https.
|
||||
if pubKeyID.Scheme != "https" { |
||||
return false, errors.New("federated servers must use https: " + pubKeyID.String()) |
||||
} |
||||
|
||||
signature := request.Header.Get("signature") |
||||
var algorithmString string |
||||
signatureComponents := strings.Split(signature, ",") |
||||
for _, component := range signatureComponents { |
||||
kv := strings.Split(component, "=") |
||||
if kv[0] == "algorithm" { |
||||
algorithmString = kv[1] |
||||
break |
||||
} |
||||
} |
||||
|
||||
algorithmString = strings.Trim(algorithmString, "\"") |
||||
if algorithmString == "" { |
||||
return false, errors.New("Unable to determine algorithm to verify request") |
||||
} |
||||
|
||||
actor, err := resolvers.GetResolvedActorFromIRI(pubKeyID.String()) |
||||
if err != nil { |
||||
return false, errors.Wrap(err, "failed to resolve actor from IRI to fetch key") |
||||
} |
||||
|
||||
// Test to see if the actor is in the list of blocked federated domains.
|
||||
if isBlockedDomain(actor.ActorIri.Hostname()) { |
||||
return false, errors.New("domain is blocked") |
||||
} |
||||
|
||||
// If actor is specifically blocked, then fail validation.
|
||||
if blocked, err := isBlockedActor(actor.ActorIri); err != nil || blocked { |
||||
return false, err |
||||
} |
||||
|
||||
key := actor.W3IDSecurityV1PublicKey.Begin().Get().GetW3IDSecurityV1PublicKeyPem().Get() |
||||
block, _ := pem.Decode([]byte(key)) |
||||
if block == nil { |
||||
log.Errorln("failed to parse PEM block containing the public key") |
||||
return false, errors.New("failed to parse PEM block containing the public key") |
||||
} |
||||
|
||||
parsedKey, err := x509.ParsePKIXPublicKey(block.Bytes) |
||||
if err != nil { |
||||
log.Errorln("failed to parse DER encoded public key: " + err.Error()) |
||||
return false, errors.Wrap(err, "failed to parse DER encoded public key") |
||||
} |
||||
|
||||
var algorithm httpsig.Algorithm = httpsig.Algorithm(algorithmString) |
||||
|
||||
// The verifier will verify the Digest in addition to the HTTP signature
|
||||
if err := verifier.Verify(parsedKey, algorithm); err != nil { |
||||
log.Warnln("verification error for", pubKeyID, err) |
||||
return false, errors.Wrap(err, "verification error: "+pubKeyID.String()) |
||||
} |
||||
|
||||
return true, nil |
||||
} |
||||
|
||||
func isBlockedDomain(domain string) bool { |
||||
blockedDomains := data.GetBlockedFederatedDomains() |
||||
|
||||
for _, blockedDomain := range blockedDomains { |
||||
if strings.Contains(domain, blockedDomain) { |
||||
return true |
||||
} |
||||
} |
||||
|
||||
return false |
||||
} |
||||
|
||||
func isBlockedActor(actorIRI *url.URL) (bool, error) { |
||||
blockedactor, err := persistence.GetFollower(actorIRI.String()) |
||||
|
||||
if blockedactor != nil && blockedactor.DisabledAt != nil { |
||||
return true, errors.Wrap(err, "remote actor is blocked") |
||||
} |
||||
|
||||
return false, nil |
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"net/url" |
||||
"testing" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
func makeFakePerson() vocab.ActivityStreamsPerson { |
||||
iri, _ := url.Parse("https://freedom.eagle/user/mrfoo") |
||||
name := "Mr Foo" |
||||
username := "foodawg" |
||||
inbox, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/inbox") |
||||
userAvatarURL, _ := url.Parse("https://fake.fediverse.server/user/mrfoo/avatar.png") |
||||
|
||||
person := streams.NewActivityStreamsPerson() |
||||
|
||||
id := streams.NewJSONLDIdProperty() |
||||
id.Set(iri) |
||||
person.SetJSONLDId(id) |
||||
|
||||
nameProperty := streams.NewActivityStreamsNameProperty() |
||||
nameProperty.AppendXMLSchemaString(name) |
||||
person.SetActivityStreamsName(nameProperty) |
||||
|
||||
preferredUsernameProperty := streams.NewActivityStreamsPreferredUsernameProperty() |
||||
preferredUsernameProperty.SetXMLSchemaString(username) |
||||
person.SetActivityStreamsPreferredUsername(preferredUsernameProperty) |
||||
|
||||
inboxProp := streams.NewActivityStreamsInboxProperty() |
||||
inboxProp.SetIRI(inbox) |
||||
person.SetActivityStreamsInbox(inboxProp) |
||||
|
||||
image := streams.NewActivityStreamsImage() |
||||
imgProp := streams.NewActivityStreamsUrlProperty() |
||||
imgProp.AppendIRI(userAvatarURL) |
||||
image.SetActivityStreamsUrl(imgProp) |
||||
icon := streams.NewActivityStreamsIconProperty() |
||||
icon.AppendActivityStreamsImage(image) |
||||
person.SetActivityStreamsIcon(icon) |
||||
|
||||
return person |
||||
} |
||||
|
||||
func TestMain(m *testing.M) { |
||||
data.SetupPersistence(":memory:") |
||||
data.SetServerURL("https://my.cool.site.biz") |
||||
persistence.Setup(data.GetDatastore()) |
||||
m.Run() |
||||
} |
||||
|
||||
func TestBlockedDomains(t *testing.T) { |
||||
person := makeFakePerson() |
||||
|
||||
data.SetBlockedFederatedDomains([]string{"freedom.eagle", "guns.life"}) |
||||
|
||||
if len(data.GetBlockedFederatedDomains()) != 2 { |
||||
t.Error("Blocked federated domains is not set correctly") |
||||
} |
||||
|
||||
for _, domain := range data.GetBlockedFederatedDomains() { |
||||
if domain == person.GetJSONLDId().GetIRI().Host { |
||||
return |
||||
} |
||||
} |
||||
|
||||
t.Error("Failed to catch blocked domain") |
||||
} |
||||
|
||||
func TestBlockedActors(t *testing.T) { |
||||
person := makeFakePerson() |
||||
persistence.AddFollow(apmodels.ActivityPubActor{ |
||||
ActorIri: person.GetJSONLDId().GetIRI(), |
||||
Inbox: person.GetJSONLDId().GetIRI(), |
||||
FollowRequestIri: person.GetJSONLDId().GetIRI(), |
||||
}, false) |
||||
persistence.BlockOrRejectFollower(person.GetJSONLDId().GetIRI().String()) |
||||
|
||||
blocked, err := isBlockedActor(person.GetJSONLDId().GetIRI()) |
||||
if err != nil { |
||||
t.Error(err) |
||||
return |
||||
} |
||||
|
||||
if !blocked { |
||||
t.Error("Failed to block actor") |
||||
} |
||||
|
||||
failedBlockIRI, _ := url.Parse("https://freedom.eagle/user/mrbar") |
||||
failedBlock, err := isBlockedActor(failedBlockIRI) |
||||
|
||||
if failedBlock { |
||||
t.Error("Invalid blocking of unblocked actor IRI") |
||||
} |
||||
} |
@ -0,0 +1,44 @@
@@ -0,0 +1,44 @@
|
||||
package inbox |
||||
|
||||
import ( |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
const ( |
||||
// InboxWorkerPoolSize defines the number of concurrent ActivityPub handlers.
|
||||
InboxWorkerPoolSize = 10 |
||||
) |
||||
|
||||
// Job struct bundling the ActivityPub and the payload in one struct.
|
||||
type Job struct { |
||||
request apmodels.InboxRequest |
||||
} |
||||
|
||||
var queue chan Job |
||||
|
||||
// InitInboxWorkerPool starts n go routines that await ActivityPub jobs.
|
||||
func InitInboxWorkerPool() { |
||||
queue = make(chan Job) |
||||
|
||||
// start workers
|
||||
for i := 1; i <= InboxWorkerPoolSize; i++ { |
||||
go worker(i, queue) |
||||
} |
||||
} |
||||
|
||||
// AddToQueue will queue up an outbound http request.
|
||||
func AddToQueue(req apmodels.InboxRequest) { |
||||
log.Tracef("Queued request for ActivityPub inbox handler") |
||||
queue <- Job{req} |
||||
} |
||||
|
||||
func worker(workerID int, queue <-chan Job) { |
||||
log.Debugf("Started ActivityPub worker %d", workerID) |
||||
|
||||
for job := range queue { |
||||
handle(job.request) |
||||
|
||||
log.Tracef("Done with ActivityPub inbox handler using worker %d", workerID) |
||||
} |
||||
} |
@ -0,0 +1,245 @@
@@ -0,0 +1,245 @@
|
||||
package outbox |
||||
|
||||
import ( |
||||
"errors" |
||||
"fmt" |
||||
"net/url" |
||||
"path/filepath" |
||||
"regexp" |
||||
"strings" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/activitypub/workerpool" |
||||
|
||||
"github.com/owncast/owncast/config" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/utils" |
||||
log "github.com/sirupsen/logrus" |
||||
"github.com/teris-io/shortid" |
||||
) |
||||
|
||||
// SendLive will send all followers the message saying you started a live stream.
|
||||
func SendLive() error { |
||||
textContent := data.GetFederationGoLiveMessage() |
||||
|
||||
// If the message is empty then do not send it.
|
||||
if textContent == "" { |
||||
return nil |
||||
} |
||||
|
||||
tagStrings := []string{} |
||||
reg := regexp.MustCompile("[^a-zA-Z0-9]+") |
||||
|
||||
tagProp := streams.NewActivityStreamsTagProperty() |
||||
for _, tagString := range data.GetServerMetadataTags() { |
||||
tagWithoutSpecialCharacters := reg.ReplaceAllString(tagString, "") |
||||
hashtag := apmodels.MakeHashtag(tagWithoutSpecialCharacters) |
||||
tagProp.AppendTootHashtag(hashtag) |
||||
tagString := getHashtagLinkHTMLFromTagString(tagWithoutSpecialCharacters) |
||||
tagStrings = append(tagStrings, tagString) |
||||
} |
||||
|
||||
// Manually add Owncast hashtag if it doesn't already exist so it shows up
|
||||
// in Owncast search results.
|
||||
// We can remove this down the road, but it'll be nice for now.
|
||||
if _, exists := utils.FindInSlice(tagStrings, "owncast"); !exists { |
||||
hashtag := apmodels.MakeHashtag("owncast") |
||||
tagProp.AppendTootHashtag(hashtag) |
||||
} |
||||
|
||||
tagsString := strings.Join(tagStrings, " ") |
||||
|
||||
var streamTitle string |
||||
if title := data.GetStreamTitle(); title != "" { |
||||
streamTitle = fmt.Sprintf("<p>%s</p>", title) |
||||
} |
||||
textContent = fmt.Sprintf("<p>%s</p><p>%s</p><p>%s</p><a href=\"%s\">%s</a>", textContent, streamTitle, tagsString, data.GetServerURL(), data.GetServerURL()) |
||||
|
||||
activity, _, note, noteID := createBaseOutboundMessage(textContent) |
||||
|
||||
note.SetActivityStreamsTag(tagProp) |
||||
|
||||
// Attach an image along with the Federated message.
|
||||
previewURL, err := url.Parse(data.GetServerURL()) |
||||
if err == nil { |
||||
var imageToAttach string |
||||
previewGif := filepath.Join(config.WebRoot, "preview.gif") |
||||
thumbnailJpg := filepath.Join(config.WebRoot, "thumbnail.jpg") |
||||
|
||||
if utils.DoesFileExists(previewGif) { |
||||
imageToAttach = "preview.gif" |
||||
} else if utils.DoesFileExists(thumbnailJpg) { |
||||
imageToAttach = "thumbnail.jpg" |
||||
} |
||||
if imageToAttach != "" { |
||||
previewURL.Path = imageToAttach |
||||
apmodels.AddImageAttachmentToNote(note, previewURL.String()) |
||||
} |
||||
} |
||||
|
||||
if data.GetNSFW() { |
||||
// Mark content as sensitive.
|
||||
sensitive := streams.NewActivityStreamsSensitiveProperty() |
||||
sensitive.AppendXMLSchemaBoolean(true) |
||||
note.SetActivityStreamsSensitive(sensitive) |
||||
} |
||||
|
||||
b, err := apmodels.Serialize(activity) |
||||
if err != nil { |
||||
log.Errorln("unable to serialize go live message activity", err) |
||||
return errors.New("unable to serialize go live message activity " + err.Error()) |
||||
} |
||||
|
||||
if err := SendToFollowers(b); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := Add(note, noteID, true); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// SendPublicMessage will send a public message to all followers.
|
||||
func SendPublicMessage(textContent string) error { |
||||
originalContent := textContent |
||||
textContent = utils.RenderSimpleMarkdown(textContent) |
||||
|
||||
tagProp := streams.NewActivityStreamsTagProperty() |
||||
|
||||
// Iterate through the post text and find #Hashtags.
|
||||
words := strings.Split(originalContent, " ") |
||||
for _, word := range words { |
||||
if strings.HasPrefix(word, "#") { |
||||
tagWithoutHashtag := strings.TrimPrefix(word, "#") |
||||
|
||||
// Replace the instances of the tag with a link to the tag page.
|
||||
tagHTML := getHashtagLinkHTMLFromTagString(tagWithoutHashtag) |
||||
textContent = strings.ReplaceAll(textContent, word, tagHTML) |
||||
|
||||
// Create Hashtag object for the tag.
|
||||
hashtag := apmodels.MakeHashtag(tagWithoutHashtag) |
||||
tagProp.AppendTootHashtag(hashtag) |
||||
} |
||||
} |
||||
|
||||
activity, _, note, noteID := createBaseOutboundMessage(textContent) |
||||
note.SetActivityStreamsTag(tagProp) |
||||
|
||||
b, err := apmodels.Serialize(activity) |
||||
if err != nil { |
||||
log.Errorln("unable to serialize custom fediverse message activity", err) |
||||
return errors.New("unable to serialize custom fediverse message activity " + err.Error()) |
||||
} |
||||
|
||||
if err := SendToFollowers(b); err != nil { |
||||
return err |
||||
} |
||||
|
||||
if err := Add(note, noteID, false); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// nolint: unparam
|
||||
func createBaseOutboundMessage(textContent string) (vocab.ActivityStreamsCreate, string, vocab.ActivityStreamsNote, string) { |
||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername()) |
||||
noteID := shortid.MustGenerate() |
||||
noteIRI := apmodels.MakeLocalIRIForResource(noteID) |
||||
id := shortid.MustGenerate() |
||||
activity := apmodels.CreateCreateActivity(id, localActor) |
||||
object := streams.NewActivityStreamsObjectProperty() |
||||
activity.SetActivityStreamsObject(object) |
||||
|
||||
note := apmodels.MakeNote(textContent, noteIRI, localActor) |
||||
object.AppendActivityStreamsNote(note) |
||||
|
||||
return activity, id, note, noteID |
||||
} |
||||
|
||||
// Get Hashtag HTML link for a given tag (without # prefix).
|
||||
func getHashtagLinkHTMLFromTagString(baseHashtag string) string { |
||||
return fmt.Sprintf("<a class=\"hashtag\" href=\"https://directory.owncast.online/tags/%s\">#%s</a>", baseHashtag, baseHashtag) |
||||
} |
||||
|
||||
// SendToFollowers will send an arbitrary payload to all follower inboxes.
|
||||
func SendToFollowers(payload []byte) error { |
||||
localActor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername()) |
||||
|
||||
followers, err := persistence.GetFederationFollowers(-1, 0) |
||||
if err != nil { |
||||
log.Errorln("unable to fetch followers to send to", err) |
||||
return errors.New("unable to fetch followers to send payload to") |
||||
} |
||||
|
||||
for _, follower := range followers { |
||||
inbox, _ := url.Parse(follower.Inbox) |
||||
req, err := requests.CreateSignedRequest(payload, inbox, localActor) |
||||
if err != nil { |
||||
log.Errorln("unable to create outbox request", follower.Inbox, err) |
||||
return errors.New("unable to create outbox request: " + follower.Inbox) |
||||
} |
||||
|
||||
workerpool.AddToOutboundQueue(req) |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// UpdateFollowersWithAccountUpdates will send an update to all followers alerting of a profile update.
|
||||
func UpdateFollowersWithAccountUpdates() error { |
||||
// Don't do anything if federation is disabled.
|
||||
if !data.GetFederationEnabled() { |
||||
return nil |
||||
} |
||||
|
||||
id := shortid.MustGenerate() |
||||
objectID := apmodels.MakeLocalIRIForResource(id) |
||||
activity := apmodels.MakeUpdateActivity(objectID) |
||||
|
||||
actor := streams.NewActivityStreamsPerson() |
||||
actorID := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername()) |
||||
actorIDProperty := streams.NewJSONLDIdProperty() |
||||
actorIDProperty.Set(actorID) |
||||
actor.SetJSONLDId(actorIDProperty) |
||||
|
||||
actorProperty := streams.NewActivityStreamsActorProperty() |
||||
actorProperty.AppendActivityStreamsPerson(actor) |
||||
activity.SetActivityStreamsActor(actorProperty) |
||||
|
||||
obj := streams.NewActivityStreamsObjectProperty() |
||||
obj.AppendIRI(actorID) |
||||
activity.SetActivityStreamsObject(obj) |
||||
|
||||
b, err := apmodels.Serialize(activity) |
||||
if err != nil { |
||||
log.Errorln("unable to serialize send update actor activity", err) |
||||
return errors.New("unable to serialize send update actor activity") |
||||
} |
||||
return SendToFollowers(b) |
||||
} |
||||
|
||||
// Add will save an ActivityPub object to the datastore.
|
||||
func Add(item vocab.Type, id string, isLiveNotification bool) error { |
||||
iri := item.GetJSONLDId().GetIRI().String() |
||||
typeString := item.GetTypeName() |
||||
|
||||
if iri == "" { |
||||
log.Errorln("Unable to get iri from item") |
||||
return errors.New("Unable to get iri from item " + id) |
||||
} |
||||
|
||||
b, err := apmodels.Serialize(item) |
||||
if err != nil { |
||||
log.Errorln("unable to serialize model when saving to outbox", err) |
||||
return err |
||||
} |
||||
|
||||
return persistence.AddToOutbox(iri, b, typeString, isLiveNotification) |
||||
} |
@ -0,0 +1,121 @@
@@ -0,0 +1,121 @@
|
||||
package persistence |
||||
|
||||
import ( |
||||
"context" |
||||
|
||||
"github.com/owncast/owncast/db" |
||||
"github.com/owncast/owncast/models" |
||||
"github.com/owncast/owncast/utils" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func createFederationFollowersTable() { |
||||
log.Traceln("Creating federation followers table...") |
||||
|
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_followers ( |
||||
"iri" TEXT NOT NULL, |
||||
"inbox" TEXT NOT NULL, |
||||
"name" TEXT, |
||||
"username" TEXT NOT NULL, |
||||
"image" TEXT, |
||||
"request" TEXT NOT NULL, |
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
"approved_at" TIMESTAMP, |
||||
"disabled_at" TIMESTAMP, |
||||
PRIMARY KEY (iri)); |
||||
CREATE INDEX iri_index ON ap_followers (iri); |
||||
CREATE INDEX approved_at_index ON ap_followers (approved_at);` |
||||
|
||||
stmt, err := _datastore.DB.Prepare(createTableSQL) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer stmt.Close() |
||||
_, err = stmt.Exec() |
||||
if err != nil { |
||||
log.Warnln("error executing sql creating followers table", createTableSQL, err) |
||||
} |
||||
} |
||||
|
||||
// GetFollowerCount will return the number of followers we're keeping track of.
|
||||
func GetFollowerCount() (int64, error) { |
||||
ctx := context.Background() |
||||
return _datastore.GetQueries().GetFollowerCount(ctx) |
||||
} |
||||
|
||||
// GetFederationFollowers will return a slice of the followers we keep track of locally.
|
||||
func GetFederationFollowers(limit int, offset int) ([]models.Follower, error) { |
||||
ctx := context.Background() |
||||
followersResult, err := _datastore.GetQueries().GetFederationFollowersWithOffset(ctx, db.GetFederationFollowersWithOffsetParams{ |
||||
Limit: int32(limit), |
||||
Offset: int32(offset), |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
followers := make([]models.Follower, 0) |
||||
|
||||
for _, row := range followersResult { |
||||
singleFollower := models.Follower{ |
||||
Name: row.Name.String, |
||||
Username: row.Username, |
||||
Image: row.Image.String, |
||||
ActorIRI: row.Iri, |
||||
Inbox: row.Inbox, |
||||
Timestamp: utils.NullTime(row.CreatedAt), |
||||
} |
||||
|
||||
followers = append(followers, singleFollower) |
||||
} |
||||
|
||||
return followers, nil |
||||
} |
||||
|
||||
// GetPendingFollowRequests will return pending follow requests.
|
||||
func GetPendingFollowRequests() ([]models.Follower, error) { |
||||
pendingFollowersResult, err := _datastore.GetQueries().GetFederationFollowerApprovalRequests(context.Background()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
followers := make([]models.Follower, 0) |
||||
|
||||
for _, row := range pendingFollowersResult { |
||||
singleFollower := models.Follower{ |
||||
Name: row.Name.String, |
||||
Username: row.Username, |
||||
Image: row.Image.String, |
||||
ActorIRI: row.Iri, |
||||
Inbox: row.Inbox, |
||||
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true}, |
||||
} |
||||
followers = append(followers, singleFollower) |
||||
} |
||||
|
||||
return followers, nil |
||||
} |
||||
|
||||
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
||||
func GetBlockedAndRejectedFollowers() ([]models.Follower, error) { |
||||
pendingFollowersResult, err := _datastore.GetQueries().GetRejectedAndBlockedFollowers(context.Background()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
followers := make([]models.Follower, 0) |
||||
|
||||
for _, row := range pendingFollowersResult { |
||||
singleFollower := models.Follower{ |
||||
Name: row.Name.String, |
||||
Username: row.Username, |
||||
Image: row.Image.String, |
||||
ActorIRI: row.Iri, |
||||
DisabledAt: utils.NullTime{Time: row.DisabledAt.Time, Valid: true}, |
||||
Timestamp: utils.NullTime{Time: row.CreatedAt.Time, Valid: true}, |
||||
} |
||||
followers = append(followers, singleFollower) |
||||
} |
||||
|
||||
return followers, nil |
||||
} |
@ -0,0 +1,360 @@
@@ -0,0 +1,360 @@
|
||||
package persistence |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"fmt" |
||||
"net/url" |
||||
"time" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/resolvers" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/db" |
||||
"github.com/owncast/owncast/models" |
||||
"github.com/pkg/errors" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
var _datastore *data.Datastore |
||||
|
||||
// Setup will initialize the ActivityPub persistence layer with the provided datastore.
|
||||
func Setup(datastore *data.Datastore) { |
||||
_datastore = datastore |
||||
createFederationFollowersTable() |
||||
createFederationOutboxTable() |
||||
createFederatedActivitiesTable() |
||||
} |
||||
|
||||
// AddFollow will save a follow to the datastore.
|
||||
func AddFollow(follow apmodels.ActivityPubActor, approved bool) error { |
||||
log.Traceln("Saving", follow.ActorIri, "as a follower.") |
||||
var image string |
||||
if follow.Image != nil { |
||||
image = follow.Image.String() |
||||
} |
||||
return createFollow(follow.ActorIri.String(), follow.Inbox.String(), follow.FollowRequestIri.String(), follow.Name, follow.Username, image, approved) |
||||
} |
||||
|
||||
// RemoveFollow will remove a follow from the datastore.
|
||||
func RemoveFollow(unfollow apmodels.ActivityPubActor) error { |
||||
log.Traceln("Removing", unfollow.ActorIri, "as a follower.") |
||||
return removeFollow(unfollow.ActorIri) |
||||
} |
||||
|
||||
// GetFollower will return a single follower/request given an IRI.
|
||||
func GetFollower(iri string) (*apmodels.ActivityPubActor, error) { |
||||
result, err := _datastore.GetQueries().GetFollowerByIRI(context.Background(), iri) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
followIRI, err := url.Parse(result.Request) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "error parsing follow request IRI") |
||||
} |
||||
|
||||
iriURL, err := url.Parse(result.Iri) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "error parsing actor IRI") |
||||
} |
||||
|
||||
inbox, err := url.Parse(result.Inbox) |
||||
if err != nil { |
||||
return nil, errors.Wrap(err, "error parsing acting inbox") |
||||
} |
||||
|
||||
image, _ := url.Parse(result.Image.String) |
||||
|
||||
var disabledAt *time.Time |
||||
if result.DisabledAt.Valid { |
||||
disabledAt = &result.DisabledAt.Time |
||||
} |
||||
|
||||
follower := apmodels.ActivityPubActor{ |
||||
ActorIri: iriURL, |
||||
Inbox: inbox, |
||||
Name: result.Name.String, |
||||
Username: result.Username, |
||||
Image: image, |
||||
FollowRequestIri: followIRI, |
||||
DisabledAt: disabledAt, |
||||
} |
||||
|
||||
return &follower, nil |
||||
} |
||||
|
||||
// ApprovePreviousFollowRequest will approve a follow request.
|
||||
func ApprovePreviousFollowRequest(iri string) error { |
||||
return _datastore.GetQueries().ApproveFederationFollower(context.Background(), db.ApproveFederationFollowerParams{ |
||||
Iri: iri, |
||||
ApprovedAt: sql.NullTime{ |
||||
Time: time.Now(), |
||||
Valid: true, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
// BlockOrRejectFollower will block an existing follower or reject a follow request.
|
||||
func BlockOrRejectFollower(iri string) error { |
||||
return _datastore.GetQueries().RejectFederationFollower(context.Background(), db.RejectFederationFollowerParams{ |
||||
Iri: iri, |
||||
DisabledAt: sql.NullTime{ |
||||
Time: time.Now(), |
||||
Valid: true, |
||||
}, |
||||
}) |
||||
} |
||||
|
||||
func createFollow(actor string, inbox string, request string, name string, username string, image string, approved bool) error { |
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
var approvedAt sql.NullTime |
||||
if approved { |
||||
approvedAt = sql.NullTime{ |
||||
Time: time.Now(), |
||||
Valid: true, |
||||
} |
||||
} |
||||
|
||||
if err = _datastore.GetQueries().WithTx(tx).AddFollower(context.Background(), db.AddFollowerParams{ |
||||
Iri: actor, |
||||
Inbox: inbox, |
||||
Name: sql.NullString{String: name, Valid: true}, |
||||
Username: username, |
||||
Image: sql.NullString{String: image, Valid: true}, |
||||
ApprovedAt: approvedAt, |
||||
Request: request, |
||||
}); err != nil { |
||||
log.Errorln("error creating new federation follow: ", err) |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// UpdateFollower will update the details of a stored follower given an IRI.
|
||||
func UpdateFollower(actorIRI string, inbox string, name string, username string, image string) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
if err = _datastore.GetQueries().WithTx(tx).UpdateFollowerByIRI(context.Background(), db.UpdateFollowerByIRIParams{ |
||||
Inbox: inbox, |
||||
Name: sql.NullString{String: name, Valid: true}, |
||||
Username: username, |
||||
Image: sql.NullString{String: image, Valid: true}, |
||||
Iri: actorIRI, |
||||
}); err != nil { |
||||
return fmt.Errorf("error updating follower %s %s", actorIRI, err) |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
func removeFollow(actor *url.URL) error { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
if err := _datastore.GetQueries().WithTx(tx).RemoveFollowerByIRI(context.Background(), actor.String()); err != nil { |
||||
return err |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// createFederatedActivitiesTable will create the accepted
|
||||
// activities table if needed.
|
||||
func createFederatedActivitiesTable() { |
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_accepted_activities ( |
||||
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, |
||||
"iri" TEXT NOT NULL, |
||||
"actor" TEXT NOT NULL, |
||||
"type" TEXT NOT NULL, |
||||
"timestamp" TIMESTAMP NOT NULL |
||||
); |
||||
CREATE INDEX iri_actor_index ON ap_accepted_activities (iri,actor);` |
||||
|
||||
stmt, err := _datastore.DB.Prepare(createTableSQL) |
||||
if err != nil { |
||||
log.Fatal("error creating inbox table", err) |
||||
} |
||||
defer stmt.Close() |
||||
if _, err := stmt.Exec(); err != nil { |
||||
log.Fatal("error creating inbound federated activities table", err) |
||||
} |
||||
} |
||||
|
||||
func createFederationOutboxTable() { |
||||
log.Traceln("Creating federation outbox table...") |
||||
createTableSQL := `CREATE TABLE IF NOT EXISTS ap_outbox ( |
||||
"iri" TEXT NOT NULL, |
||||
"value" BLOB, |
||||
"type" TEXT NOT NULL, |
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
"live_notification" BOOLEAN DEFAULT FALSE, |
||||
PRIMARY KEY (iri)); |
||||
CREATE INDEX iri ON ap_outbox (iri); |
||||
CREATE INDEX type ON ap_outbox (type); |
||||
CREATE INDEX live_notification ON ap_outbox (live_notification);` |
||||
|
||||
stmt, err := _datastore.DB.Prepare(createTableSQL) |
||||
if err != nil { |
||||
log.Fatal(err) |
||||
} |
||||
defer stmt.Close() |
||||
_, err = stmt.Exec() |
||||
if err != nil { |
||||
log.Warnln("error executing sql creating outbox table", createTableSQL, err) |
||||
} |
||||
} |
||||
|
||||
// GetOutboxPostCount will return the number of posts in the outbox.
|
||||
func GetOutboxPostCount() (int64, error) { |
||||
ctx := context.Background() |
||||
return _datastore.GetQueries().GetLocalPostCount(ctx) |
||||
} |
||||
|
||||
// GetOutbox will return an instance of the outbox populated by stored items.
|
||||
func GetOutbox(limit int, offset int) (vocab.ActivityStreamsOrderedCollection, error) { |
||||
collection := streams.NewActivityStreamsOrderedCollection() |
||||
orderedItems := streams.NewActivityStreamsOrderedItemsProperty() |
||||
rows, err := _datastore.GetQueries().GetOutboxWithOffset( |
||||
context.Background(), |
||||
db.GetOutboxWithOffsetParams{Limit: int32(limit), Offset: int32(offset)}, |
||||
) |
||||
if err != nil { |
||||
return collection, err |
||||
} |
||||
|
||||
for _, value := range rows { |
||||
createCallback := func(c context.Context, activity vocab.ActivityStreamsCreate) error { |
||||
orderedItems.AppendActivityStreamsCreate(activity) |
||||
return nil |
||||
} |
||||
if err := resolvers.Resolve(context.Background(), value, createCallback); err != nil { |
||||
return collection, err |
||||
} |
||||
} |
||||
|
||||
return collection, nil |
||||
} |
||||
|
||||
// AddToOutbox will store a single payload to the persistence layer.
|
||||
func AddToOutbox(iri string, itemData []byte, typeString string, isLiveNotification bool) error { |
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
} |
||||
defer func() { |
||||
_ = tx.Rollback() |
||||
}() |
||||
|
||||
if err = _datastore.GetQueries().WithTx(tx).AddToOutbox(context.Background(), db.AddToOutboxParams{ |
||||
Iri: iri, |
||||
Value: itemData, |
||||
Type: typeString, |
||||
LiveNotification: sql.NullBool{Bool: isLiveNotification, Valid: true}, |
||||
}); err != nil { |
||||
return fmt.Errorf("error creating new item in federation outbox %s", err) |
||||
} |
||||
|
||||
return tx.Commit() |
||||
} |
||||
|
||||
// GetObjectByID will return a string representation of a single object by the ID.
|
||||
func GetObjectByID(id string) (string, error) { |
||||
value, err := _datastore.GetQueries().GetObjectFromOutboxByID(context.Background(), id) |
||||
return string(value), err |
||||
} |
||||
|
||||
// GetObjectByIRI will return a string representation of a single object by the IRI.
|
||||
func GetObjectByIRI(iri string) (string, bool, time.Time, error) { |
||||
row, err := _datastore.GetQueries().GetObjectFromOutboxByIRI(context.Background(), iri) |
||||
return string(row.Value), row.LiveNotification.Bool, row.CreatedAt.Time, err |
||||
} |
||||
|
||||
// GetLocalPostCount will return the number of posts existing locally.
|
||||
func GetLocalPostCount() (int64, error) { |
||||
ctx := context.Background() |
||||
return _datastore.GetQueries().GetLocalPostCount(ctx) |
||||
} |
||||
|
||||
// SaveInboundFediverseActivity will save an event to the ap_inbound_activities table.
|
||||
func SaveInboundFediverseActivity(objectIRI string, actorIRI string, eventType string, timestamp time.Time) error { |
||||
if err := _datastore.GetQueries().AddToAcceptedActivities(context.Background(), db.AddToAcceptedActivitiesParams{ |
||||
Iri: objectIRI, |
||||
Actor: actorIRI, |
||||
Type: eventType, |
||||
Timestamp: timestamp, |
||||
}); err != nil { |
||||
return errors.Wrap(err, "error saving event "+objectIRI) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// GetInboundActivities will return a collection of saved, federated activities
|
||||
// limited and offset by the values provided to support pagination.
|
||||
func GetInboundActivities(limit int, offset int) ([]models.FederatedActivity, error) { |
||||
ctx := context.Background() |
||||
rows, err := _datastore.GetQueries().GetInboundActivitiesWithOffset(ctx, db.GetInboundActivitiesWithOffsetParams{ |
||||
Limit: int32(limit), |
||||
Offset: int32(offset), |
||||
}) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
activities := make([]models.FederatedActivity, 0) |
||||
|
||||
for _, row := range rows { |
||||
singleActivity := models.FederatedActivity{ |
||||
IRI: row.Iri, |
||||
ActorIRI: row.Actor, |
||||
Type: row.Type, |
||||
Timestamp: row.Timestamp, |
||||
} |
||||
activities = append(activities, singleActivity) |
||||
} |
||||
|
||||
return activities, nil |
||||
} |
||||
|
||||
// HasPreviouslyHandledInboundActivity will return if we have previously handled
|
||||
// an inbound federated activity.
|
||||
func HasPreviouslyHandledInboundActivity(iri string, actorIRI string, eventType string) (bool, error) { |
||||
exists, err := _datastore.GetQueries().DoesInboundActivityExist(context.Background(), db.DoesInboundActivityExistParams{ |
||||
Iri: iri, |
||||
Actor: actorIRI, |
||||
Type: eventType, |
||||
}) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
return exists > 0, nil |
||||
} |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
package requests |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/url" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/workerpool" |
||||
|
||||
"github.com/teris-io/shortid" |
||||
) |
||||
|
||||
// SendFollowAccept will send an accept activity to a follow request from a specified local user.
|
||||
func SendFollowAccept(inbox *url.URL, followRequestIRI *url.URL, fromLocalAccountName string) error { |
||||
followAccept := makeAcceptFollow(followRequestIRI, fromLocalAccountName) |
||||
localAccountIRI := apmodels.MakeLocalIRIForAccount(fromLocalAccountName) |
||||
|
||||
var jsonmap map[string]interface{} |
||||
jsonmap, _ = streams.Serialize(followAccept) |
||||
b, _ := json.Marshal(jsonmap) |
||||
req, err := CreateSignedRequest(b, inbox, localAccountIRI) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
workerpool.AddToOutboundQueue(req) |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func makeAcceptFollow(followRequestIri *url.URL, fromAccountName string) vocab.ActivityStreamsAccept { |
||||
acceptIDString := shortid.MustGenerate() |
||||
acceptID := apmodels.MakeLocalIRIForResource(acceptIDString) |
||||
actorID := apmodels.MakeLocalIRIForAccount(fromAccountName) |
||||
|
||||
accept := streams.NewActivityStreamsAccept() |
||||
idProperty := streams.NewJSONLDIdProperty() |
||||
idProperty.SetIRI(acceptID) |
||||
accept.SetJSONLDId(idProperty) |
||||
|
||||
actor := apmodels.MakeActorPropertyWithID(actorID) |
||||
accept.SetActivityStreamsActor(actor) |
||||
|
||||
object := streams.NewActivityStreamsObjectProperty() |
||||
object.AppendIRI(followRequestIri) |
||||
accept.SetActivityStreamsObject(object) |
||||
|
||||
return accept |
||||
} |
@ -0,0 +1,75 @@
@@ -0,0 +1,75 @@
|
||||
package requests |
||||
|
||||
import ( |
||||
"bytes" |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
|
||||
"github.com/owncast/owncast/config" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// WriteStreamResponse will write a ActivityPub object to the provided ResponseWriter and sign with the provided key.
|
||||
func WriteStreamResponse(item vocab.Type, w http.ResponseWriter, publicKey crypto.PublicKey) error { |
||||
var jsonmap map[string]interface{} |
||||
jsonmap, _ = streams.Serialize(item) |
||||
b, err := json.Marshal(jsonmap) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return WriteResponse(b, w, publicKey) |
||||
} |
||||
|
||||
// WritePayloadResponse will write any arbitrary object to the provided ResponseWriter and sign with the provided key.
|
||||
func WritePayloadResponse(payload interface{}, w http.ResponseWriter, publicKey crypto.PublicKey) error { |
||||
b, err := json.Marshal(payload) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
return WriteResponse(b, w, publicKey) |
||||
} |
||||
|
||||
// WriteResponse will write any arbitrary payload to the provided ResponseWriter and sign with the provided key.
|
||||
func WriteResponse(payload []byte, w http.ResponseWriter, publicKey crypto.PublicKey) error { |
||||
w.Header().Set("Content-Type", "application/activity+json") |
||||
|
||||
if err := crypto.SignResponse(w, payload, publicKey); err != nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
log.Errorln("unable to sign response", err) |
||||
return err |
||||
} |
||||
|
||||
if _, err := w.Write(payload); err != nil { |
||||
w.WriteHeader(http.StatusInternalServerError) |
||||
return err |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// CreateSignedRequest will create a signed POST request of a payload to the provided destination.
|
||||
func CreateSignedRequest(payload []byte, url *url.URL, fromActorIRI *url.URL) (*http.Request, error) { |
||||
log.Debugln("Sending", string(payload), "to", url) |
||||
|
||||
req, _ := http.NewRequest("POST", url.String(), bytes.NewBuffer(payload)) |
||||
|
||||
ua := fmt.Sprintf("%s; https://owncast.online", config.GetReleaseString()) |
||||
req.Header.Set("User-Agent", ua) |
||||
req.Header.Set("Content-Type", "application/activity+json") |
||||
|
||||
if err := crypto.SignRequest(req, payload, fromActorIRI); err != nil { |
||||
log.Errorln("error signing request:", err) |
||||
return nil, err |
||||
} |
||||
|
||||
return req, nil |
||||
} |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
package resolvers |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"fmt" |
||||
|
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
func getPersonFromFollow(activity vocab.ActivityStreamsFollow) (apmodels.ActivityPubActor, error) { |
||||
return GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) |
||||
} |
||||
|
||||
// MakeFollowRequest will convert an inbound Follow request to our internal actor model.
|
||||
func MakeFollowRequest(c context.Context, activity vocab.ActivityStreamsFollow) (*apmodels.ActivityPubActor, error) { |
||||
person, err := getPersonFromFollow(activity) |
||||
if err != nil { |
||||
return nil, errors.New("unable to resolve person from follow request: " + err.Error()) |
||||
} |
||||
|
||||
hostname := person.ActorIri.Hostname() |
||||
username := person.Username |
||||
fullUsername := fmt.Sprintf("%s@%s", username, hostname) |
||||
|
||||
followRequest := apmodels.ActivityPubActor{ |
||||
ActorIri: person.ActorIri, |
||||
FollowRequestIri: activity.GetJSONLDId().Get(), |
||||
Inbox: person.Inbox, |
||||
Name: person.Name, |
||||
Username: fullUsername, |
||||
Image: person.Image, |
||||
} |
||||
|
||||
return &followRequest, nil |
||||
} |
||||
|
||||
// MakeUnFollowRequest will convert an inbound Unfollow request to our internal actor model.
|
||||
func MakeUnFollowRequest(c context.Context, activity vocab.ActivityStreamsUndo) *apmodels.ActivityPubActor { |
||||
person, err := GetResolvedActorFromActorProperty(activity.GetActivityStreamsActor()) |
||||
if err != nil { |
||||
log.Errorln("unable to resolve person from actor iri", person.ActorIri, err) |
||||
return nil |
||||
} |
||||
|
||||
unfollowRequest := apmodels.ActivityPubActor{ |
||||
ActorIri: person.ActorIri, |
||||
FollowRequestIri: activity.GetJSONLDId().Get(), |
||||
Inbox: person.Inbox, |
||||
Name: person.Name, |
||||
Image: person.Image, |
||||
} |
||||
|
||||
return &unfollowRequest |
||||
} |
@ -0,0 +1,125 @@
@@ -0,0 +1,125 @@
|
||||
package resolvers |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"io/ioutil" |
||||
"net/http" |
||||
|
||||
"github.com/go-fed/activity/streams" |
||||
"github.com/go-fed/activity/streams/vocab" |
||||
"github.com/owncast/owncast/activitypub/apmodels" |
||||
"github.com/owncast/owncast/activitypub/crypto" |
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/pkg/errors" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// Resolve will translate a raw ActivityPub payload and fire the callback associated with that activity type.
|
||||
func Resolve(c context.Context, data []byte, callbacks ...interface{}) error { |
||||
jsonResolver, err := streams.NewJSONResolver(callbacks...) |
||||
if err != nil { |
||||
// Something in the setup was wrong. For example, a callback has an
|
||||
// unsupported signature and would never be called
|
||||
return err |
||||
} |
||||
|
||||
var jsonMap map[string]interface{} |
||||
if err = json.Unmarshal(data, &jsonMap); err != nil { |
||||
return err |
||||
} |
||||
|
||||
log.Debugln("Resolving payload...", string(data)) |
||||
|
||||
// The createCallback function will be called.
|
||||
err = jsonResolver.Resolve(c, jsonMap) |
||||
if err != nil && !streams.IsUnmatchedErr(err) { |
||||
// Something went wrong
|
||||
return err |
||||
} else if streams.IsUnmatchedErr(err) { |
||||
// Everything went right but the callback didn't match or the ActivityStreams
|
||||
// type is one that wasn't code generated.
|
||||
log.Debugln("No match: ", err) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
// ResolveIRI will resolve an IRI ahd call the correct callback for the resolved type.
|
||||
func ResolveIRI(c context.Context, iri string, callbacks ...interface{}) error { |
||||
log.Debugln("Resolving", iri) |
||||
|
||||
req, _ := http.NewRequest("GET", iri, nil) |
||||
|
||||
actor := apmodels.MakeLocalIRIForAccount(data.GetDefaultFederationUsername()) |
||||
if err := crypto.SignRequest(req, nil, actor); err != nil { |
||||
return err |
||||
} |
||||
|
||||
response, err := http.DefaultClient.Do(req) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer response.Body.Close() |
||||
|
||||
data, err := ioutil.ReadAll(response.Body) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
// fmt.Println(string(data))
|
||||
return Resolve(c, data, callbacks...) |
||||
} |
||||
|
||||
// GetResolvedActorFromActorProperty resolve an actor property to a fully populated person.
|
||||
func GetResolvedActorFromActorProperty(actor vocab.ActivityStreamsActorProperty) (apmodels.ActivityPubActor, error) { |
||||
var err error |
||||
var apActor apmodels.ActivityPubActor |
||||
|
||||
personCallback := func(c context.Context, person vocab.ActivityStreamsPerson) error { |
||||
apActor = apmodels.MakeActorFromPerson(person) |
||||
return nil |
||||
} |
||||
|
||||
serviceCallback := func(c context.Context, s vocab.ActivityStreamsService) error { |
||||
apActor = apmodels.MakeActorFromService(s) |
||||
return nil |
||||
} |
||||
|
||||
for iter := actor.Begin(); iter != actor.End(); iter = iter.Next() { |
||||
if iter.IsIRI() { |
||||
iri := iter.GetIRI() |
||||
if e := ResolveIRI(context.Background(), iri.String(), personCallback, serviceCallback); e != nil { |
||||
err = e |
||||
} |
||||
} else if iter.IsActivityStreamsPerson() { |
||||
person := iter.GetActivityStreamsPerson() |
||||
apActor = apmodels.MakeActorFromPerson(person) |
||||
} |
||||
} |
||||
|
||||
return apActor, errors.Wrap(err, "unable to resolve actor from actor property") |
||||
} |
||||
|
||||
// GetResolvedActorFromIRI will resolve an IRI string to a fully populated actor.
|
||||
func GetResolvedActorFromIRI(personOrServiceIRI string) (apmodels.ActivityPubActor, error) { |
||||
var err error |
||||
var apActor apmodels.ActivityPubActor |
||||
|
||||
personCallback := func(c context.Context, person vocab.ActivityStreamsPerson) error { |
||||
apActor = apmodels.MakeActorFromPerson(person) |
||||
return nil |
||||
} |
||||
|
||||
serviceCallback := func(c context.Context, s vocab.ActivityStreamsService) error { |
||||
apActor = apmodels.MakeActorFromService(s) |
||||
return nil |
||||
} |
||||
|
||||
if e := ResolveIRI(context.Background(), personOrServiceIRI, personCallback, serviceCallback); e != nil { |
||||
err = e |
||||
} |
||||
|
||||
return apActor, errors.Wrap(err, "unable to resolve actor from IRI string: "+personOrServiceIRI) |
||||
} |
@ -0,0 +1,35 @@
@@ -0,0 +1,35 @@
|
||||
package activitypub |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/activitypub/controllers" |
||||
"github.com/owncast/owncast/router/middleware" |
||||
) |
||||
|
||||
// StartRouter will start the federation specific http router.
|
||||
func StartRouter() { |
||||
// WebFinger
|
||||
http.HandleFunc("/.well-known/webfinger", controllers.WebfingerHandler) |
||||
|
||||
// Host Metadata
|
||||
http.HandleFunc("/.well-known/host-meta", controllers.HostMetaController) |
||||
|
||||
// Nodeinfo v1
|
||||
http.HandleFunc("/.well-known/nodeinfo", controllers.NodeInfoController) |
||||
|
||||
// x-nodeinfo v2
|
||||
http.HandleFunc("/.well-known/x-nodeinfo2", controllers.XNodeInfo2Controller) |
||||
|
||||
// Nodeinfo v2
|
||||
http.HandleFunc("/nodeinfo/2.0", controllers.NodeInfoV2Controller) |
||||
|
||||
// Instance details
|
||||
http.HandleFunc("/api/v1/instance", controllers.InstanceV1Controller) |
||||
|
||||
// Single ActivityPub Actor
|
||||
http.HandleFunc("/federation/user/", middleware.RequireActivityPubOrRedirect(controllers.ActorHandler)) |
||||
|
||||
// Single AP object
|
||||
http.HandleFunc("/federation/", middleware.RequireActivityPubOrRedirect(controllers.ObjectHandler)) |
||||
} |
@ -0,0 +1,66 @@
@@ -0,0 +1,66 @@
|
||||
package workerpool |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
const ( |
||||
// ActivityPubWorkerPoolSize defines the number of concurrent HTTP ActivityPub requests.
|
||||
ActivityPubWorkerPoolSize = 10 |
||||
) |
||||
|
||||
// Job struct bundling the ActivityPub and the payload in one struct.
|
||||
type Job struct { |
||||
request *http.Request |
||||
} |
||||
|
||||
var queue chan Job |
||||
|
||||
// InitOutboundWorkerPool starts n go routines that await ActivityPub jobs.
|
||||
func InitOutboundWorkerPool() { |
||||
queue = make(chan Job) |
||||
|
||||
// start workers
|
||||
for i := 1; i <= ActivityPubWorkerPoolSize; i++ { |
||||
go worker(i, queue) |
||||
} |
||||
} |
||||
|
||||
// AddToOutboundQueue will queue up an outbound http request.
|
||||
func AddToOutboundQueue(req *http.Request) { |
||||
log.Tracef("Queued request for ActivityPub destination %s", req.RequestURI) |
||||
queue <- Job{req} |
||||
} |
||||
|
||||
func worker(workerID int, queue <-chan Job) { |
||||
log.Debugf("Started ActivityPub worker %d", workerID) |
||||
|
||||
for job := range queue { |
||||
if err := sendActivityPubMessageToInbox(job); err != nil { |
||||
log.Errorf("ActivityPub destination %s failed to send Error: %s", job.request.RequestURI, err) |
||||
} |
||||
log.Tracef("Done with ActivityPub destination %s using worker %d", job.request.RequestURI, workerID) |
||||
} |
||||
} |
||||
|
||||
func sendActivityPubMessageToInbox(job Job) error { |
||||
// req, err := http.NewRequest("POST", job.inbox.String(), bytes.NewReader(job.payload))
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
// req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
client := &http.Client{} |
||||
|
||||
resp, err := client.Do(job.request) |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
defer resp.Body.Close() |
||||
|
||||
return nil |
||||
} |
@ -0,0 +1,171 @@
@@ -0,0 +1,171 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/activitypub" |
||||
"github.com/owncast/owncast/activitypub/outbox" |
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/controllers" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
// SendFederatedMessage will send a manual message to the fediverse.
|
||||
func SendFederatedMessage(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValue, success := getValueFromRequest(w, r) |
||||
if !success { |
||||
return |
||||
} |
||||
|
||||
message, ok := configValue.Value.(string) |
||||
if !ok { |
||||
controllers.WriteSimpleResponse(w, false, "unable to send message") |
||||
} |
||||
|
||||
if err := activitypub.SendPublicFederatedMessage(message); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "sent") |
||||
} |
||||
|
||||
// SetFederationEnabled will set if Federation features are enabled.
|
||||
func SetFederationEnabled(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValue, success := getValueFromRequest(w, r) |
||||
if !success { |
||||
return |
||||
} |
||||
|
||||
if err := data.SetFederationEnabled(configValue.Value.(bool)); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
controllers.WriteSimpleResponse(w, true, "federation features saved") |
||||
} |
||||
|
||||
// SetFederationActivityPrivate will set if Federation features are private to followers.
|
||||
func SetFederationActivityPrivate(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValue, success := getValueFromRequest(w, r) |
||||
if !success { |
||||
return |
||||
} |
||||
|
||||
if err := data.SetFederationIsPrivate(configValue.Value.(bool)); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
// Update Fediverse followers about this change.
|
||||
if err := outbox.UpdateFollowersWithAccountUpdates(); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "federation private saved") |
||||
} |
||||
|
||||
// SetFederationShowEngagement will set if Fedivese engagement shows in chat.
|
||||
func SetFederationShowEngagement(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValue, success := getValueFromRequest(w, r) |
||||
if !success { |
||||
return |
||||
} |
||||
|
||||
if err := data.SetFederationShowEngagement(configValue.Value.(bool)); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
controllers.WriteSimpleResponse(w, true, "federation show engagement saved") |
||||
} |
||||
|
||||
// SetFederationUsername will set the local actor username used for federation activities.
|
||||
func SetFederationUsername(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValue, success := getValueFromRequest(w, r) |
||||
if !success { |
||||
return |
||||
} |
||||
|
||||
if err := data.SetFederationUsername(configValue.Value.(string)); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "username saved") |
||||
} |
||||
|
||||
// SetFederationGoLiveMessage will set the federated message sent when the streamer goes live.
|
||||
func SetFederationGoLiveMessage(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValue, success := getValueFromRequest(w, r) |
||||
if !success { |
||||
return |
||||
} |
||||
|
||||
if err := data.SetFederationGoLiveMessage(configValue.Value.(string)); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "message saved") |
||||
} |
||||
|
||||
// SetFederationBlockDomains saves a list of domains to block on the Fediverse.
|
||||
func SetFederationBlockDomains(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
configValues, success := getValuesFromRequest(w, r) |
||||
if !success { |
||||
controllers.WriteSimpleResponse(w, false, "unable to handle provided domains") |
||||
return |
||||
} |
||||
|
||||
domainStrings := make([]string, 0) |
||||
for _, domain := range configValues { |
||||
domainStrings = append(domainStrings, domain.Value.(string)) |
||||
} |
||||
|
||||
if err := data.SetBlockedFederatedDomains(domainStrings); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "saved") |
||||
} |
||||
|
||||
// GetFederatedActions will return the saved list of accepted inbound
|
||||
// federated activities.
|
||||
func GetFederatedActions(w http.ResponseWriter, r *http.Request) { |
||||
activities, err := persistence.GetInboundActivities(100, 0) |
||||
if err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteResponse(w, activities) |
||||
} |
@ -0,0 +1,82 @@
@@ -0,0 +1,82 @@
|
||||
package admin |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
"github.com/owncast/owncast/activitypub/requests" |
||||
"github.com/owncast/owncast/controllers" |
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
// ApproveFollower will approve a federated follow request.
|
||||
func ApproveFollower(w http.ResponseWriter, r *http.Request) { |
||||
if !requirePOST(w, r) { |
||||
return |
||||
} |
||||
|
||||
type approveFollowerRequest struct { |
||||
ActorIRI string `json:"actorIRI"` |
||||
Approved bool `json:"approved"` |
||||
} |
||||
|
||||
decoder := json.NewDecoder(r.Body) |
||||
var approval approveFollowerRequest |
||||
if err := decoder.Decode(&approval); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, "unable to handle follower state with provided values") |
||||
return |
||||
} |
||||
|
||||
if approval.Approved { |
||||
// Approve a follower
|
||||
if err := persistence.ApprovePreviousFollowRequest(approval.ActorIRI); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
localAccountName := data.GetDefaultFederationUsername() |
||||
|
||||
follower, err := persistence.GetFollower(approval.ActorIRI) |
||||
if err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
// Send the approval to the follow requestor.
|
||||
if err := requests.SendFollowAccept(follower.Inbox, follower.FollowRequestIri, localAccountName); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
} else { |
||||
// Remove/block a follower
|
||||
if err := persistence.BlockOrRejectFollower(approval.ActorIRI); err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
} |
||||
|
||||
controllers.WriteSimpleResponse(w, true, "follower updated") |
||||
} |
||||
|
||||
// GetPendingFollowRequests will return a list of pending follow requests.
|
||||
func GetPendingFollowRequests(w http.ResponseWriter, r *http.Request) { |
||||
requests, err := persistence.GetPendingFollowRequests() |
||||
if err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteResponse(w, requests) |
||||
} |
||||
|
||||
// GetBlockedAndRejectedFollowers will return blocked and rejected followers.
|
||||
func GetBlockedAndRejectedFollowers(w http.ResponseWriter, r *http.Request) { |
||||
rejections, err := persistence.GetBlockedAndRejectedFollowers() |
||||
if err != nil { |
||||
controllers.WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
controllers.WriteResponse(w, rejections) |
||||
} |
@ -0,0 +1,18 @@
@@ -0,0 +1,18 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"net/http" |
||||
|
||||
"github.com/owncast/owncast/activitypub/persistence" |
||||
) |
||||
|
||||
// GetFollowers will handle an API request to fetch the list of followers (non-activitypub response).
|
||||
func GetFollowers(w http.ResponseWriter, r *http.Request) { |
||||
followers, err := persistence.GetFederationFollowers(-1, 0) |
||||
if err != nil { |
||||
WriteSimpleResponse(w, false, "unable to fetch followers") |
||||
return |
||||
} |
||||
|
||||
WriteResponse(w, followers) |
||||
} |
@ -0,0 +1,100 @@
@@ -0,0 +1,100 @@
|
||||
package controllers |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"fmt" |
||||
"net/http" |
||||
"net/url" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
) |
||||
|
||||
// RemoteFollow handles a request to begin the remote follow redirect flow.
|
||||
func RemoteFollow(w http.ResponseWriter, r *http.Request) { |
||||
type followRequest struct { |
||||
Account string `json:"account"` |
||||
} |
||||
|
||||
type followResponse struct { |
||||
RedirectURL string `json:"redirectUrl"` |
||||
} |
||||
|
||||
var request followRequest |
||||
decoder := json.NewDecoder(r.Body) |
||||
if err := decoder.Decode(&request); err != nil { |
||||
WriteSimpleResponse(w, false, "unable to parse request") |
||||
return |
||||
} |
||||
|
||||
if request.Account == "" { |
||||
WriteSimpleResponse(w, false, "Remote Fediverse account is required to follow.") |
||||
return |
||||
} |
||||
|
||||
localActorPath, _ := url.Parse(data.GetServerURL()) |
||||
localActorPath.Path = fmt.Sprintf("/federation/user/%s", data.GetDefaultFederationUsername()) |
||||
var template string |
||||
links, err := getWebfingerLinks(request.Account) |
||||
if err != nil { |
||||
WriteSimpleResponse(w, false, err.Error()) |
||||
return |
||||
} |
||||
|
||||
// Acquire the remote follow redirect template.
|
||||
for _, link := range links { |
||||
for k, v := range link { |
||||
if k == "rel" && v == "http://ostatus.org/schema/1.0/subscribe" && link["template"] != nil { |
||||
template = link["template"].(string) |
||||
} |
||||
} |
||||
} |
||||
|
||||
if localActorPath.String() == "" || template == "" { |
||||
WriteSimpleResponse(w, false, "unable to determine remote follow information for "+request.Account) |
||||
return |
||||
} |
||||
|
||||
redirectURL := strings.Replace(template, "{uri}", localActorPath.String(), 1) |
||||
response := followResponse{ |
||||
RedirectURL: redirectURL, |
||||
} |
||||
|
||||
WriteResponse(w, response) |
||||
} |
||||
|
||||
func getWebfingerLinks(account string) ([]map[string]interface{}, error) { |
||||
type webfingerResponse struct { |
||||
Links []map[string]interface{} `json:"links"` |
||||
} |
||||
|
||||
account = strings.TrimLeft(account, "@") // remove any leading @
|
||||
accountComponents := strings.Split(account, "@") |
||||
fediverseServer := accountComponents[1] |
||||
|
||||
// HTTPS is required.
|
||||
requestURL, err := url.Parse("https://" + fediverseServer) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("unable to parse fediverse server host %s", fediverseServer) |
||||
} |
||||
|
||||
requestURL.Path = "/.well-known/webfinger" |
||||
query := requestURL.Query() |
||||
query.Add("resource", fmt.Sprintf("acct:%s", account)) |
||||
requestURL.RawQuery = query.Encode() |
||||
|
||||
response, err := http.DefaultClient.Get(requestURL.String()) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
defer response.Body.Close() |
||||
|
||||
var links webfingerResponse |
||||
decoder := json.NewDecoder(response.Body) |
||||
if err := decoder.Decode(&links); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return links.Links, nil |
||||
} |
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
package events |
||||
|
||||
import "github.com/owncast/owncast/core/data" |
||||
|
||||
// FediverseEngagementEvent is a message displayed in chat on representing an action on the Fediverse.
|
||||
type FediverseEngagementEvent struct { |
||||
Event |
||||
MessageEvent |
||||
Image *string `json:"image"` |
||||
Link string `json:"link"` |
||||
UserAccountName string `json:"title"` |
||||
} |
||||
|
||||
// GetBroadcastPayload will return the object to send to all chat users.
|
||||
func (e *FediverseEngagementEvent) GetBroadcastPayload() EventPayload { |
||||
return EventPayload{ |
||||
"id": e.ID, |
||||
"timestamp": e.Timestamp, |
||||
"body": e.Body, |
||||
"image": e.Image, |
||||
"type": e.Event.Type, |
||||
"title": e.UserAccountName, |
||||
"user": EventPayload{ |
||||
"displayName": data.GetServerName(), |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// GetMessageType will return the event type for this message.
|
||||
func (e *FediverseEngagementEvent) GetMessageType() EventType { |
||||
return e.Event.Type |
||||
} |
@ -0,0 +1,39 @@
@@ -0,0 +1,39 @@
|
||||
package chat |
||||
|
||||
import ( |
||||
"fmt" |
||||
|
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// Only keep recent messages so we don't keep more chat data than needed
|
||||
// for privacy and efficiency reasons.
|
||||
func runPruner() { |
||||
_datastore.DbLock.Lock() |
||||
defer _datastore.DbLock.Unlock() |
||||
|
||||
log.Traceln("Removing chat messages older than", maxBacklogHours, "hours") |
||||
|
||||
deleteStatement := `DELETE FROM messages WHERE timestamp <= datetime('now', 'localtime', ?)` |
||||
tx, err := _datastore.DB.Begin() |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
|
||||
stmt, err := tx.Prepare(deleteStatement) |
||||
if err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
defer stmt.Close() |
||||
|
||||
if _, err = stmt.Exec(fmt.Sprintf("-%d hours", maxBacklogHours)); err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
if err = tx.Commit(); err != nil { |
||||
log.Debugln(err) |
||||
return |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package data |
||||
|
||||
// GetFederatedInboxMap is a mapping between account names and their outbox.
|
||||
func GetFederatedInboxMap() map[string]string { |
||||
return map[string]string{ |
||||
GetDefaultFederationUsername(): GetDefaultFederationUsername(), |
||||
} |
||||
} |
||||
|
||||
// GetDefaultFederationUsername will return the username used for sending federation activities.
|
||||
func GetDefaultFederationUsername() string { |
||||
return GetFederationUsername() |
||||
} |
@ -0,0 +1,23 @@
@@ -0,0 +1,23 @@
|
||||
package data |
||||
|
||||
// GetPublicKey will return the public key.
|
||||
func GetPublicKey() string { |
||||
value, _ := _datastore.GetString(publicKeyKey) |
||||
return value |
||||
} |
||||
|
||||
// SetPublicKey will save the public key.
|
||||
func SetPublicKey(key string) error { |
||||
return _datastore.SetString(publicKeyKey, key) |
||||
} |
||||
|
||||
// GetPrivateKey will return the private key.
|
||||
func GetPrivateKey() string { |
||||
value, _ := _datastore.GetString(privateKeyKey) |
||||
return value |
||||
} |
||||
|
||||
// SetPrivateKey will save the private key.
|
||||
func SetPrivateKey(key string) error { |
||||
return _datastore.SetString(privateKeyKey, key) |
||||
} |
@ -0,0 +1,28 @@
@@ -0,0 +1,28 @@
|
||||
# SQL Queries |
||||
|
||||
sqlc generates **type-safe code** from SQL. Here's how it works: |
||||
|
||||
1. You define the schema in `schema.sql`. |
||||
1. You write your queries in `query.sql` using regular SQL. |
||||
1. You run `sqlc generate` to generate Go code with type-safe interfaces to those queries. |
||||
1. You write application code that calls the generated code. |
||||
|
||||
Only those who need to create or update SQL queries will need to have `sqlc` installed on their system. **It is not a dependency required to build the codebase.** |
||||
|
||||
## Install sqlc |
||||
|
||||
### Snap |
||||
|
||||
`sudo snap install sqlc` |
||||
|
||||
### Go install |
||||
|
||||
`go install github.com/kyleconroy/sqlc/cmd/sqlc@latest` |
||||
|
||||
### macOS |
||||
|
||||
`brew install sqlc` |
||||
|
||||
### Download a release |
||||
|
||||
Visit <https://github.com/kyleconroy/sqlc/releases> to download a release for your environment. |
@ -0,0 +1,29 @@
@@ -0,0 +1,29 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
|
||||
package db |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
) |
||||
|
||||
type DBTX interface { |
||||
ExecContext(context.Context, string, ...interface{}) (sql.Result, error) |
||||
PrepareContext(context.Context, string) (*sql.Stmt, error) |
||||
QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) |
||||
QueryRowContext(context.Context, string, ...interface{}) *sql.Row |
||||
} |
||||
|
||||
func New(db DBTX) *Queries { |
||||
return &Queries{db: db} |
||||
} |
||||
|
||||
type Queries struct { |
||||
db DBTX |
||||
} |
||||
|
||||
func (q *Queries) WithTx(tx *sql.Tx) *Queries { |
||||
return &Queries{ |
||||
db: tx, |
||||
} |
||||
} |
@ -0,0 +1,36 @@
@@ -0,0 +1,36 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
|
||||
package db |
||||
|
||||
import ( |
||||
"database/sql" |
||||
"time" |
||||
) |
||||
|
||||
type ApAcceptedActivity struct { |
||||
ID int32 |
||||
Iri string |
||||
Actor string |
||||
Type string |
||||
Timestamp time.Time |
||||
} |
||||
|
||||
type ApFollower struct { |
||||
Iri string |
||||
Inbox string |
||||
Name sql.NullString |
||||
Username string |
||||
Image sql.NullString |
||||
Request string |
||||
CreatedAt sql.NullTime |
||||
ApprovedAt sql.NullTime |
||||
DisabledAt sql.NullTime |
||||
} |
||||
|
||||
type ApOutbox struct { |
||||
Iri string |
||||
Value []byte |
||||
Type string |
||||
CreatedAt sql.NullTime |
||||
LiveNotification sql.NullBool |
||||
} |
@ -0,0 +1,57 @@
@@ -0,0 +1,57 @@
|
||||
-- Queries added to query.sql must be compiled into Go code with sqlc. Read README.md for details. |
||||
|
||||
-- Federation related queries. |
||||
|
||||
-- name: GetFollowerCount :one |
||||
SElECT count(*) FROM ap_followers WHERE approved_at is not null; |
||||
|
||||
-- name: GetLocalPostCount :one |
||||
SElECT count(*) FROM ap_outbox; |
||||
|
||||
-- name: GetFederationFollowersWithOffset :many |
||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null LIMIT $1 OFFSET $2; |
||||
|
||||
-- name: GetRejectedAndBlockedFollowers :many |
||||
SELECT iri, name, username, image, created_at, disabled_at FROM ap_followers WHERE disabled_at is not null; |
||||
|
||||
-- name: GetFederationFollowerApprovalRequests :many |
||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null; |
||||
|
||||
-- name: ApproveFederationFollower :exec |
||||
UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2; |
||||
|
||||
-- name: RejectFederationFollower :exec |
||||
UPDATE ap_followers SET approved_at = null, disabled_at = $1 WHERE iri = $2; |
||||
|
||||
-- name: GetFollowerByIRI :one |
||||
SELECT iri, inbox, name, username, image, request, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1; |
||||
|
||||
-- name: GetOutboxWithOffset :many |
||||
SELECT value FROM ap_outbox LIMIT $1 OFFSET $2; |
||||
|
||||
-- name: GetObjectFromOutboxByID :one |
||||
SELECT value FROM ap_outbox WHERE iri = $1; |
||||
|
||||
-- name: GetObjectFromOutboxByIRI :one |
||||
SELECT value, live_notification, created_at FROM ap_outbox WHERE iri = $1; |
||||
|
||||
-- name: RemoveFollowerByIRI :exec |
||||
DELETE FROM ap_followers WHERE iri = $1; |
||||
|
||||
-- name: AddFollower :exec |
||||
INSERT INTO ap_followers(iri, inbox, request, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7); |
||||
|
||||
-- name: AddToOutbox :exec |
||||
INSERT INTO ap_outbox(iri, value, type, live_notification) values($1, $2, $3, $4); |
||||
|
||||
-- name: AddToAcceptedActivities :exec |
||||
INSERT INTO ap_accepted_activities(iri, actor, type, timestamp) values($1, $2, $3, $4); |
||||
|
||||
-- name: GetInboundActivitiesWithOffset :many |
||||
SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestamp DESC LIMIT $1 OFFSET $2; |
||||
|
||||
-- name: DoesInboundActivityExist :one |
||||
SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TYPE = $3; |
||||
|
||||
-- name: UpdateFollowerByIRI :exec |
||||
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5; |
@ -0,0 +1,441 @@
@@ -0,0 +1,441 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// source: query.sql
|
||||
|
||||
package db |
||||
|
||||
import ( |
||||
"context" |
||||
"database/sql" |
||||
"time" |
||||
) |
||||
|
||||
const addFollower = `-- name: AddFollower :exec |
||||
INSERT INTO ap_followers(iri, inbox, request, name, username, image, approved_at) values($1, $2, $3, $4, $5, $6, $7) |
||||
` |
||||
|
||||
type AddFollowerParams struct { |
||||
Iri string |
||||
Inbox string |
||||
Request string |
||||
Name sql.NullString |
||||
Username string |
||||
Image sql.NullString |
||||
ApprovedAt sql.NullTime |
||||
} |
||||
|
||||
func (q *Queries) AddFollower(ctx context.Context, arg AddFollowerParams) error { |
||||
_, err := q.db.ExecContext(ctx, addFollower, |
||||
arg.Iri, |
||||
arg.Inbox, |
||||
arg.Request, |
||||
arg.Name, |
||||
arg.Username, |
||||
arg.Image, |
||||
arg.ApprovedAt, |
||||
) |
||||
return err |
||||
} |
||||
|
||||
const addToAcceptedActivities = `-- name: AddToAcceptedActivities :exec |
||||
INSERT INTO ap_accepted_activities(iri, actor, type, timestamp) values($1, $2, $3, $4) |
||||
` |
||||
|
||||
type AddToAcceptedActivitiesParams struct { |
||||
Iri string |
||||
Actor string |
||||
Type string |
||||
Timestamp time.Time |
||||
} |
||||
|
||||
func (q *Queries) AddToAcceptedActivities(ctx context.Context, arg AddToAcceptedActivitiesParams) error { |
||||
_, err := q.db.ExecContext(ctx, addToAcceptedActivities, |
||||
arg.Iri, |
||||
arg.Actor, |
||||
arg.Type, |
||||
arg.Timestamp, |
||||
) |
||||
return err |
||||
} |
||||
|
||||
const addToOutbox = `-- name: AddToOutbox :exec |
||||
INSERT INTO ap_outbox(iri, value, type, live_notification) values($1, $2, $3, $4) |
||||
` |
||||
|
||||
type AddToOutboxParams struct { |
||||
Iri string |
||||
Value []byte |
||||
Type string |
||||
LiveNotification sql.NullBool |
||||
} |
||||
|
||||
func (q *Queries) AddToOutbox(ctx context.Context, arg AddToOutboxParams) error { |
||||
_, err := q.db.ExecContext(ctx, addToOutbox, |
||||
arg.Iri, |
||||
arg.Value, |
||||
arg.Type, |
||||
arg.LiveNotification, |
||||
) |
||||
return err |
||||
} |
||||
|
||||
const approveFederationFollower = `-- name: ApproveFederationFollower :exec |
||||
UPDATE ap_followers SET approved_at = $1, disabled_at = null WHERE iri = $2 |
||||
` |
||||
|
||||
type ApproveFederationFollowerParams struct { |
||||
ApprovedAt sql.NullTime |
||||
Iri string |
||||
} |
||||
|
||||
func (q *Queries) ApproveFederationFollower(ctx context.Context, arg ApproveFederationFollowerParams) error { |
||||
_, err := q.db.ExecContext(ctx, approveFederationFollower, arg.ApprovedAt, arg.Iri) |
||||
return err |
||||
} |
||||
|
||||
const doesInboundActivityExist = `-- name: DoesInboundActivityExist :one |
||||
SELECT count(*) FROM ap_accepted_activities WHERE iri = $1 AND actor = $2 AND TYPE = $3 |
||||
` |
||||
|
||||
type DoesInboundActivityExistParams struct { |
||||
Iri string |
||||
Actor string |
||||
Type string |
||||
} |
||||
|
||||
func (q *Queries) DoesInboundActivityExist(ctx context.Context, arg DoesInboundActivityExistParams) (int64, error) { |
||||
row := q.db.QueryRowContext(ctx, doesInboundActivityExist, arg.Iri, arg.Actor, arg.Type) |
||||
var count int64 |
||||
err := row.Scan(&count) |
||||
return count, err |
||||
} |
||||
|
||||
const getFederationFollowerApprovalRequests = `-- name: GetFederationFollowerApprovalRequests :many |
||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at IS null AND disabled_at is null |
||||
` |
||||
|
||||
type GetFederationFollowerApprovalRequestsRow struct { |
||||
Iri string |
||||
Inbox string |
||||
Name sql.NullString |
||||
Username string |
||||
Image sql.NullString |
||||
CreatedAt sql.NullTime |
||||
} |
||||
|
||||
func (q *Queries) GetFederationFollowerApprovalRequests(ctx context.Context) ([]GetFederationFollowerApprovalRequestsRow, error) { |
||||
rows, err := q.db.QueryContext(ctx, getFederationFollowerApprovalRequests) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
var items []GetFederationFollowerApprovalRequestsRow |
||||
for rows.Next() { |
||||
var i GetFederationFollowerApprovalRequestsRow |
||||
if err := rows.Scan( |
||||
&i.Iri, |
||||
&i.Inbox, |
||||
&i.Name, |
||||
&i.Username, |
||||
&i.Image, |
||||
&i.CreatedAt, |
||||
); err != nil { |
||||
return nil, err |
||||
} |
||||
items = append(items, i) |
||||
} |
||||
if err := rows.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := rows.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
return items, nil |
||||
} |
||||
|
||||
const getFederationFollowersWithOffset = `-- name: GetFederationFollowersWithOffset :many |
||||
SELECT iri, inbox, name, username, image, created_at FROM ap_followers WHERE approved_at is not null LIMIT $1 OFFSET $2 |
||||
` |
||||
|
||||
type GetFederationFollowersWithOffsetParams struct { |
||||
Limit int32 |
||||
Offset int32 |
||||
} |
||||
|
||||
type GetFederationFollowersWithOffsetRow struct { |
||||
Iri string |
||||
Inbox string |
||||
Name sql.NullString |
||||
Username string |
||||
Image sql.NullString |
||||
CreatedAt sql.NullTime |
||||
} |
||||
|
||||
func (q *Queries) GetFederationFollowersWithOffset(ctx context.Context, arg GetFederationFollowersWithOffsetParams) ([]GetFederationFollowersWithOffsetRow, error) { |
||||
rows, err := q.db.QueryContext(ctx, getFederationFollowersWithOffset, arg.Limit, arg.Offset) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
var items []GetFederationFollowersWithOffsetRow |
||||
for rows.Next() { |
||||
var i GetFederationFollowersWithOffsetRow |
||||
if err := rows.Scan( |
||||
&i.Iri, |
||||
&i.Inbox, |
||||
&i.Name, |
||||
&i.Username, |
||||
&i.Image, |
||||
&i.CreatedAt, |
||||
); err != nil { |
||||
return nil, err |
||||
} |
||||
items = append(items, i) |
||||
} |
||||
if err := rows.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := rows.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
return items, nil |
||||
} |
||||
|
||||
const getFollowerByIRI = `-- name: GetFollowerByIRI :one |
||||
SELECT iri, inbox, name, username, image, request, created_at, approved_at, disabled_at FROM ap_followers WHERE iri = $1 |
||||
` |
||||
|
||||
func (q *Queries) GetFollowerByIRI(ctx context.Context, iri string) (ApFollower, error) { |
||||
row := q.db.QueryRowContext(ctx, getFollowerByIRI, iri) |
||||
var i ApFollower |
||||
err := row.Scan( |
||||
&i.Iri, |
||||
&i.Inbox, |
||||
&i.Name, |
||||
&i.Username, |
||||
&i.Image, |
||||
&i.Request, |
||||
&i.CreatedAt, |
||||
&i.ApprovedAt, |
||||
&i.DisabledAt, |
||||
) |
||||
return i, err |
||||
} |
||||
|
||||
const getFollowerCount = `-- name: GetFollowerCount :one |
||||
|
||||
|
||||
SElECT count(*) FROM ap_followers WHERE approved_at is not null |
||||
` |
||||
|
||||
// Queries added to query.sql must be compiled into Go code with sqlc. Read README.md for details.
|
||||
// Federation related queries.
|
||||
func (q *Queries) GetFollowerCount(ctx context.Context) (int64, error) { |
||||
row := q.db.QueryRowContext(ctx, getFollowerCount) |
||||
var count int64 |
||||
err := row.Scan(&count) |
||||
return count, err |
||||
} |
||||
|
||||
const getInboundActivitiesWithOffset = `-- name: GetInboundActivitiesWithOffset :many |
||||
SELECT iri, actor, type, timestamp FROM ap_accepted_activities ORDER BY timestamp DESC LIMIT $1 OFFSET $2 |
||||
` |
||||
|
||||
type GetInboundActivitiesWithOffsetParams struct { |
||||
Limit int32 |
||||
Offset int32 |
||||
} |
||||
|
||||
type GetInboundActivitiesWithOffsetRow struct { |
||||
Iri string |
||||
Actor string |
||||
Type string |
||||
Timestamp time.Time |
||||
} |
||||
|
||||
func (q *Queries) GetInboundActivitiesWithOffset(ctx context.Context, arg GetInboundActivitiesWithOffsetParams) ([]GetInboundActivitiesWithOffsetRow, error) { |
||||
rows, err := q.db.QueryContext(ctx, getInboundActivitiesWithOffset, arg.Limit, arg.Offset) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
var items []GetInboundActivitiesWithOffsetRow |
||||
for rows.Next() { |
||||
var i GetInboundActivitiesWithOffsetRow |
||||
if err := rows.Scan( |
||||
&i.Iri, |
||||
&i.Actor, |
||||
&i.Type, |
||||
&i.Timestamp, |
||||
); err != nil { |
||||
return nil, err |
||||
} |
||||
items = append(items, i) |
||||
} |
||||
if err := rows.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := rows.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
return items, nil |
||||
} |
||||
|
||||
const getLocalPostCount = `-- name: GetLocalPostCount :one |
||||
SElECT count(*) FROM ap_outbox |
||||
` |
||||
|
||||
func (q *Queries) GetLocalPostCount(ctx context.Context) (int64, error) { |
||||
row := q.db.QueryRowContext(ctx, getLocalPostCount) |
||||
var count int64 |
||||
err := row.Scan(&count) |
||||
return count, err |
||||
} |
||||
|
||||
const getObjectFromOutboxByID = `-- name: GetObjectFromOutboxByID :one |
||||
SELECT value FROM ap_outbox WHERE iri = $1 |
||||
` |
||||
|
||||
func (q *Queries) GetObjectFromOutboxByID(ctx context.Context, iri string) ([]byte, error) { |
||||
row := q.db.QueryRowContext(ctx, getObjectFromOutboxByID, iri) |
||||
var value []byte |
||||
err := row.Scan(&value) |
||||
return value, err |
||||
} |
||||
|
||||
const getObjectFromOutboxByIRI = `-- name: GetObjectFromOutboxByIRI :one |
||||
SELECT value, live_notification, created_at FROM ap_outbox WHERE iri = $1 |
||||
` |
||||
|
||||
type GetObjectFromOutboxByIRIRow struct { |
||||
Value []byte |
||||
LiveNotification sql.NullBool |
||||
CreatedAt sql.NullTime |
||||
} |
||||
|
||||
func (q *Queries) GetObjectFromOutboxByIRI(ctx context.Context, iri string) (GetObjectFromOutboxByIRIRow, error) { |
||||
row := q.db.QueryRowContext(ctx, getObjectFromOutboxByIRI, iri) |
||||
var i GetObjectFromOutboxByIRIRow |
||||
err := row.Scan(&i.Value, &i.LiveNotification, &i.CreatedAt) |
||||
return i, err |
||||
} |
||||
|
||||
const getOutboxWithOffset = `-- name: GetOutboxWithOffset :many |
||||
SELECT value FROM ap_outbox LIMIT $1 OFFSET $2 |
||||
` |
||||
|
||||
type GetOutboxWithOffsetParams struct { |
||||
Limit int32 |
||||
Offset int32 |
||||
} |
||||
|
||||
func (q *Queries) GetOutboxWithOffset(ctx context.Context, arg GetOutboxWithOffsetParams) ([][]byte, error) { |
||||
rows, err := q.db.QueryContext(ctx, getOutboxWithOffset, arg.Limit, arg.Offset) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
var items [][]byte |
||||
for rows.Next() { |
||||
var value []byte |
||||
if err := rows.Scan(&value); err != nil { |
||||
return nil, err |
||||
} |
||||
items = append(items, value) |
||||
} |
||||
if err := rows.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := rows.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
return items, nil |
||||
} |
||||
|
||||
const getRejectedAndBlockedFollowers = `-- name: GetRejectedAndBlockedFollowers :many |
||||
SELECT iri, name, username, image, created_at, disabled_at FROM ap_followers WHERE disabled_at is not null |
||||
` |
||||
|
||||
type GetRejectedAndBlockedFollowersRow struct { |
||||
Iri string |
||||
Name sql.NullString |
||||
Username string |
||||
Image sql.NullString |
||||
CreatedAt sql.NullTime |
||||
DisabledAt sql.NullTime |
||||
} |
||||
|
||||
func (q *Queries) GetRejectedAndBlockedFollowers(ctx context.Context) ([]GetRejectedAndBlockedFollowersRow, error) { |
||||
rows, err := q.db.QueryContext(ctx, getRejectedAndBlockedFollowers) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
defer rows.Close() |
||||
var items []GetRejectedAndBlockedFollowersRow |
||||
for rows.Next() { |
||||
var i GetRejectedAndBlockedFollowersRow |
||||
if err := rows.Scan( |
||||
&i.Iri, |
||||
&i.Name, |
||||
&i.Username, |
||||
&i.Image, |
||||
&i.CreatedAt, |
||||
&i.DisabledAt, |
||||
); err != nil { |
||||
return nil, err |
||||
} |
||||
items = append(items, i) |
||||
} |
||||
if err := rows.Close(); err != nil { |
||||
return nil, err |
||||
} |
||||
if err := rows.Err(); err != nil { |
||||
return nil, err |
||||
} |
||||
return items, nil |
||||
} |
||||
|
||||
const rejectFederationFollower = `-- name: RejectFederationFollower :exec |
||||
UPDATE ap_followers SET approved_at = null, disabled_at = $1 WHERE iri = $2 |
||||
` |
||||
|
||||
type RejectFederationFollowerParams struct { |
||||
DisabledAt sql.NullTime |
||||
Iri string |
||||
} |
||||
|
||||
func (q *Queries) RejectFederationFollower(ctx context.Context, arg RejectFederationFollowerParams) error { |
||||
_, err := q.db.ExecContext(ctx, rejectFederationFollower, arg.DisabledAt, arg.Iri) |
||||
return err |
||||
} |
||||
|
||||
const removeFollowerByIRI = `-- name: RemoveFollowerByIRI :exec |
||||
DELETE FROM ap_followers WHERE iri = $1 |
||||
` |
||||
|
||||
func (q *Queries) RemoveFollowerByIRI(ctx context.Context, iri string) error { |
||||
_, err := q.db.ExecContext(ctx, removeFollowerByIRI, iri) |
||||
return err |
||||
} |
||||
|
||||
const updateFollowerByIRI = `-- name: UpdateFollowerByIRI :exec |
||||
UPDATE ap_followers SET inbox = $1, name = $2, username = $3, image = $4 WHERE iri = $5 |
||||
` |
||||
|
||||
type UpdateFollowerByIRIParams struct { |
||||
Inbox string |
||||
Name sql.NullString |
||||
Username string |
||||
Image sql.NullString |
||||
Iri string |
||||
} |
||||
|
||||
func (q *Queries) UpdateFollowerByIRI(ctx context.Context, arg UpdateFollowerByIRIParams) error { |
||||
_, err := q.db.ExecContext(ctx, updateFollowerByIRI, |
||||
arg.Inbox, |
||||
arg.Name, |
||||
arg.Username, |
||||
arg.Image, |
||||
arg.Iri, |
||||
) |
||||
return err |
||||
} |
@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
|
||||
-- Schema update to query.sql must be referenced in queries located in query.sql |
||||
-- and compiled into code with sqlc. Read README.md for details. |
||||
|
||||
CREATE TABLE IF NOT EXISTS ap_followers ( |
||||
"iri" TEXT NOT NULL, |
||||
"inbox" TEXT NOT NULL, |
||||
"name" TEXT, |
||||
"username" TEXT NOT NULL, |
||||
"image" TEXT, |
||||
"request" TEXT NOT NULL, |
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
"approved_at" TIMESTAMP, |
||||
"disabled_at" TIMESTAMP, |
||||
PRIMARY KEY (iri)); |
||||
CREATE INDEX iri_index ON ap_followers (iri); |
||||
CREATE INDEX approved_at_index ON ap_followers (approved_at); |
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ap_outbox ( |
||||
"iri" TEXT NOT NULL, |
||||
"value" BLOB, |
||||
"type" TEXT NOT NULL, |
||||
"created_at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP, |
||||
"live_notification" BOOLEAN DEFAULT FALSE, |
||||
PRIMARY KEY (iri)); |
||||
CREATE INDEX iri ON ap_outbox (iri); |
||||
CREATE INDEX type ON ap_outbox (type); |
||||
CREATE INDEX live_notification ON ap_outbox (live_notification); |
||||
|
||||
CREATE TABLE IF NOT EXISTS ap_accepted_activities ( |
||||
"id" INTEGER NOT NULL PRIMARY KEY, |
||||
"iri" TEXT NOT NULL, |
||||
"actor" TEXT NOT NULL, |
||||
"type" TEXT NOT NULL, |
||||
"timestamp" TIMESTAMP NOT NULL |
||||
); |
||||
CREATE INDEX iri_actor_index ON ap_accepted_activities (iri,actor); |
@ -0,0 +1,12 @@
@@ -0,0 +1,12 @@
|
||||
package models |
||||
|
||||
import "time" |
||||
|
||||
// FederatedActivity is an internal representation of an activity that was
|
||||
// accepted and stored.
|
||||
type FederatedActivity struct { |
||||
IRI string `json:"iri"` |
||||
ActorIRI string `json:"actorIRI"` |
||||
Type string `json:"type"` |
||||
Timestamp time.Time `json:"timestamp"` |
||||
} |
@ -0,0 +1,21 @@
@@ -0,0 +1,21 @@
|
||||
package models |
||||
|
||||
import "github.com/owncast/owncast/utils" |
||||
|
||||
// Follower is our internal representation of a single follower within Owncast.
|
||||
type Follower struct { |
||||
// ActorIRI is the IRI of the remote actor.
|
||||
ActorIRI string `json:"link"` |
||||
// Inbox is the inbox URL of the remote follower
|
||||
Inbox string `json:"-"` |
||||
// Name is the display name of the follower.
|
||||
Name string `json:"name"` |
||||
// Username is the account username of the remote actor.
|
||||
Username string `json:"username"` |
||||
// Image is the avatar image of the follower.
|
||||
Image string `json:"image"` |
||||
// Timestamp is when this follow request was created.
|
||||
Timestamp utils.NullTime `json:"timestamp,omitempty"` |
||||
// DisabledAt is when this follower was rejected or disabled.
|
||||
DisabledAt utils.NullTime `json:"disabledAt,omitempty"` |
||||
} |
@ -0,0 +1,46 @@
@@ -0,0 +1,46 @@
|
||||
package middleware |
||||
|
||||
import ( |
||||
"net/http" |
||||
"strings" |
||||
|
||||
"github.com/owncast/owncast/core/data" |
||||
"github.com/owncast/owncast/utils" |
||||
) |
||||
|
||||
// RequireActivityPubOrRedirect will validate the requested content types and
|
||||
// redirect to the main Owncast page if it doesn't match.
|
||||
func RequireActivityPubOrRedirect(handler http.HandlerFunc) http.HandlerFunc { |
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { |
||||
if !data.GetFederationEnabled() { |
||||
w.WriteHeader(http.StatusMethodNotAllowed) |
||||
return |
||||
} |
||||
|
||||
handleAccepted := func() { |
||||
handler(w, r) |
||||
} |
||||
|
||||
acceptedContentTypes := []string{"application/json", "application/json+ld", "application/activity+json"} |
||||
acceptString := r.Header.Get("Accept") |
||||
accept := strings.Split(acceptString, ",") |
||||
|
||||
for _, singleType := range accept { |
||||
if _, accepted := utils.FindInSlice(acceptedContentTypes, strings.TrimSpace(singleType)); accepted { |
||||
handleAccepted() |
||||
return |
||||
} |
||||
} |
||||
|
||||
contentTypeString := r.Header.Get("Content-Type") |
||||
contentTypes := strings.Split(contentTypeString, ",") |
||||
for _, singleType := range contentTypes { |
||||
if _, accepted := utils.FindInSlice(acceptedContentTypes, strings.TrimSpace(singleType)); accepted { |
||||
handleAccepted() |
||||
return |
||||
} |
||||
} |
||||
|
||||
http.Redirect(w, r, "/", http.StatusTemporaryRedirect) |
||||
}) |
||||
} |
@ -0,0 +1,9 @@
@@ -0,0 +1,9 @@
|
||||
{ |
||||
"version": "1", |
||||
"packages": [{ |
||||
"schema": "db/schema.sql", |
||||
"queries": "db/query.sql", |
||||
"name": "db", |
||||
"path": "db" |
||||
}] |
||||
} |
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
self.__BUILD_MANIFEST=function(s,c,e,a,t,i,n,f,d,o,h,b,g,u){return{__rewrites:{beforeFiles:[],afterFiles:[],fallback:[]},"/":[s,c,e,a,t,i,f,"static/chunks/59-bb3486f7473684cf.js","static/chunks/pages/index-e81f0bed001d207c.js"],"/_error":["static/chunks/pages/_error-a3f18418a2205cb8.js"],"/access-tokens":[s,c,"static/chunks/pages/access-tokens-d3e9e11b79321dbb.js"],"/actions":[s,"static/chunks/pages/actions-6889c0d5d40aa70e.js"],"/chat/messages":[d,s,c,i,o,"static/chunks/pages/chat/messages-dc2695d5bac28933.js"],"/chat/users":[d,s,c,e,i,o,"static/chunks/pages/chat/users-4426bd981b718014.js"],"/config-chat":["static/chunks/pages/config-chat-3f47c2e436acea43.js"],"/config-federation":["static/chunks/674-fd7f35cd345c7a4b.js","static/chunks/pages/config-federation-ddff59205ab33383.js"],"/config-public-details":[s,n,"static/css/4da23ced01517a16.css","static/chunks/589-57c6e66ff27bec68.js",h,"static/chunks/pages/config-public-details-02c80e54bdb91852.js"],"/config-server-details":[b,"static/chunks/pages/config-server-details-40fd225da49d9b45.js"],"/config-social-items":[s,h,"static/chunks/pages/config-social-items-9ecbdce4f557ac9b.js"],"/config-storage":["static/chunks/473-2f8a49a631089460.js","static/chunks/pages/config-storage-4b8f9ff84ca4aa30.js"],"/config-video":[s,b,"static/chunks/556-4bf62bd783267914.js","static/chunks/pages/config-video-c250bf8f88dd1d1b.js"],"/federation/actions":[s,c,"static/chunks/pages/federation/actions-a817c8d84eb2e1bf.js"],"/federation/followers":[s,c,e,"static/chunks/pages/federation/followers-73207b872f42b7a6.js"],"/hardware-info":[g,c,e,a,t,n,u,"static/chunks/pages/hardware-info-4dcdf4aa6510006e.js"],"/help":[e,a,"static/chunks/132-69ec1de6a8e27de6.js","static/chunks/pages/help-3dd6da50dde27e48.js"],"/logs":[s,c,f,"static/chunks/pages/logs-d10676db469afea0.js"],"/upgrade":[s,"static/chunks/275-35d1a6aef8ecf26a.js","static/chunks/pages/upgrade-6c4ec4a032ab7232.js"],"/viewer-info":[g,c,e,a,t,n,u,"static/chunks/pages/viewer-info-a9586b5d2ecea7e8.js"],"/webhooks":[s,"static/chunks/pages/webhooks-8b96f4afcc72aba4.js"],sortedPages:["/","/_app","/_error","/access-tokens","/actions","/chat/messages","/chat/users","/config-chat","/config-federation","/config-public-details","/config-server-details","/config-social-items","/config-storage","/config-video","/federation/actions","/federation/followers","/hardware-info","/help","/logs","/upgrade","/viewer-info","/webhooks"]}}("static/chunks/829-c8d1f3db438c210b.js","static/chunks/91-5f5f536776e2d9c6.js","static/chunks/961-1db4468ca0742ea4.js","static/chunks/751-f932ff7ec3e1342a.js","static/chunks/763-6084d4b3c149b8f4.js","static/chunks/533-2f63c37b8986cca1.js","static/chunks/910-ed07ccf32f311d03.js","static/chunks/429-613793ce22468b22.js","static/chunks/29107295-2c3ce868677a27a4.js","static/chunks/464-deae2b2f674a34de.js","static/chunks/17-8c3836887f4f3962.js","static/chunks/578-b2fdca9619a3031e.js","static/chunks/36bcf0ca-c1f70baa5cd8cbbf.js","static/chunks/958-d85597c88a5651f8.js"),self.__BUILD_MANIFEST_CB&&self.__BUILD_MANIFEST_CB(); |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
self.__MIDDLEWARE_MANIFEST=[];self.__MIDDLEWARE_MANIFEST_CB&&self.__MIDDLEWARE_MANIFEST_CB() |
@ -0,0 +1 @@
@@ -0,0 +1 @@
|
||||
self.__SSG_MANIFEST=new Set,self.__SSG_MANIFEST_CB&&self.__SSG_MANIFEST_CB(); |
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue