feat: Adds a webhook for when a user follows the stream (#4362)
* feat: Adds a webhook for when a user follow the stream Frontend: Adds a checkbox which renders only when social features are enabled. Backend: - Defines a new type for the event - Implements the webhook including unit testing - Fires the webhook inside the ApproveFollower handler * Removes unnecessary Type fiels from Events struct * refactor: renames SendUserFollowedEvent func to FediverseEngagementFollow and fixes linting errors * fix: fixes unit test to reflect removed Type field from FediverseEngagementFollowEvent * feat: fires webhook also when manual approvals are not required * chore: slight cleanup --------- Co-authored-by: Gabe Kangas <gabek@real-ity.com>
This commit is contained in:
co-authored by
Gabe Kangas
parent
9cc58d14c3
commit
80850b15d0
@@ -0,0 +1,19 @@
|
||||
package events
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/teris-io/shortid"
|
||||
)
|
||||
|
||||
// Event is any kind of event.
|
||||
type Event struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
// SetDefaults will set default properties of all inbound events.
|
||||
func (e *Event) SetDefaults() {
|
||||
e.ID = shortid.MustGenerate()
|
||||
e.Timestamp = time.Now()
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package events
|
||||
|
||||
type FediverseEngagementFollowEvent struct {
|
||||
Event
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
Image string `json:"image"`
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/owncast/owncast/activitypub/requests"
|
||||
"github.com/owncast/owncast/activitypub/resolvers"
|
||||
"github.com/owncast/owncast/core/chat/events"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/persistence/configrepository"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
@@ -39,20 +40,19 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
||||
}
|
||||
|
||||
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||
actorReference := activity.GetActivityStreamsActor()
|
||||
object := activity.GetActivityStreamsObject()
|
||||
objectIRI := object.At(0).GetIRI().String()
|
||||
actorIRI := actorReference.At(0).GetIRI().String()
|
||||
|
||||
if approved {
|
||||
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
||||
log.Errorln("unable to send follow accept", err)
|
||||
return err
|
||||
}
|
||||
go webhooks.SendFediverseEngagementFollowEvent(actorIRI)
|
||||
}
|
||||
|
||||
// Save as an 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.
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"github.com/owncast/owncast/activitypub/events"
|
||||
"github.com/owncast/owncast/activitypub/persistence"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
// SendFediverseEventFollow will send a user followed event to webhook
|
||||
// destinations.
|
||||
func SendFediverseEngagementFollowEvent(iri string) {
|
||||
follower, err := persistence.GetFollower(iri)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userFollowedEvent := events.FediverseEngagementFollowEvent{}
|
||||
userFollowedEvent.SetDefaults()
|
||||
userFollowedEvent.Name = follower.Name
|
||||
userFollowedEvent.Username = follower.Username
|
||||
userFollowedEvent.Image = follower.Image.String()
|
||||
|
||||
sendFediverseEngagementEventFollow(userFollowedEvent)
|
||||
}
|
||||
|
||||
func sendFediverseEngagementEventFollow(event events.FediverseEngagementFollowEvent) {
|
||||
webhookEvent := WebhookEvent{
|
||||
Type: models.FediverseEngagementFollow,
|
||||
EventData: event,
|
||||
}
|
||||
|
||||
SendEventToWebhooks(webhookEvent)
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package webhooks
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/owncast/owncast/activitypub/events"
|
||||
"github.com/owncast/owncast/models"
|
||||
)
|
||||
|
||||
func TestSendFediverseEngagementEventFollow(t *testing.T) {
|
||||
checkPayload(t, models.FediverseEngagementFollow, func() {
|
||||
sendFediverseEngagementEventFollow(events.FediverseEngagementFollowEvent{
|
||||
Event: events.Event{
|
||||
Timestamp: time.Unix(72, 6).UTC(),
|
||||
ID: "id",
|
||||
},
|
||||
Name: "be",
|
||||
Username: "be@witch.me",
|
||||
})
|
||||
}, `{
|
||||
"id": "id",
|
||||
"image": "",
|
||||
"name": "be",
|
||||
"timestamp": "1970-01-01T00:01:12.000000006Z",
|
||||
"username": "be@witch.me"
|
||||
}`)
|
||||
}
|
||||
@@ -12,6 +12,8 @@ const (
|
||||
UserParted EventType = "USER_PARTED"
|
||||
// UserNameChanged is the event sent when a chat username change takes place.
|
||||
UserNameChanged EventType = "NAME_CHANGE"
|
||||
// FediverseEngagementFollow is the event sent when a user follows the stream.
|
||||
FediverseEngagementFollow EventType = "FEDIVERSE_ENGAGEMENT_FOLLOW"
|
||||
// VisibiltyToggled is the event sent when a chat message's visibility changes.
|
||||
VisibiltyToggled EventType = "VISIBILITY-UPDATE"
|
||||
// PING is a ping message.
|
||||
|
||||
@@ -21,6 +21,7 @@ var validEvents = []EventType{
|
||||
UserJoined,
|
||||
UserParted,
|
||||
UserNameChanged,
|
||||
FediverseEngagementFollow,
|
||||
VisibiltyToggled,
|
||||
StreamStarted,
|
||||
StreamStopped,
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { ReactElement, useEffect, useState } from 'react';
|
||||
import React, { ReactElement, useContext, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'next-export-i18n';
|
||||
import { CREATE_WEBHOOK, DELETE_WEBHOOK, fetchData, WEBHOOKS } from '../../utils/apis';
|
||||
import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/validators';
|
||||
@@ -21,6 +21,7 @@ import { Localization } from '../../types/localization';
|
||||
import { Translation } from '../../components/ui/Translation/Translation';
|
||||
|
||||
import { AdminLayout } from '../../components/layouts/AdminLayout';
|
||||
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
@@ -34,6 +35,11 @@ const availableEvents = {
|
||||
CHAT: { name: 'Chat messages', description: 'When a user sends a chat message', color: 'purple' },
|
||||
USER_JOINED: { name: 'User joined', description: 'When a user joins the chat', color: 'green' },
|
||||
USER_PARTED: { name: 'User parted', description: 'When a user leaves the chat', color: 'green' },
|
||||
FEDIVERSE_ENGAGEMENT_FOLLOW: {
|
||||
name: 'New follower',
|
||||
description: 'When a user follows the stream',
|
||||
color: 'green',
|
||||
},
|
||||
NAME_CHANGE: {
|
||||
name: 'User name changed',
|
||||
description: 'When a user changes their name',
|
||||
@@ -79,6 +85,8 @@ const NewWebhookModal = (props: Props) => {
|
||||
const [selectedEvents, setSelectedEvents] = useState([]);
|
||||
const [webhookUrl, setWebhookUrl] = useState('');
|
||||
|
||||
const { serverConfig } = useContext(ServerStatusContext);
|
||||
|
||||
const events = Object.keys(availableEvents).map(key => ({
|
||||
value: key,
|
||||
label: availableEvents[key].description,
|
||||
@@ -104,11 +112,20 @@ const NewWebhookModal = (props: Props) => {
|
||||
disabled: selectedEvents?.length === 0 || !isValidUrl(webhookUrl),
|
||||
};
|
||||
|
||||
const checkboxes = events.map(singleEvent => (
|
||||
<Col span={8} key={singleEvent.value}>
|
||||
<Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox>
|
||||
</Col>
|
||||
));
|
||||
const checkboxes = events
|
||||
.filter(singleEvent => {
|
||||
switch (singleEvent.value) {
|
||||
case 'FEDIVERSE_ENGAGEMENT_FOLLOW':
|
||||
return serverConfig.federation.enabled;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.map(singleEvent => (
|
||||
<Col span={8} key={singleEvent.value}>
|
||||
<Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox>
|
||||
</Col>
|
||||
));
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/owncast/owncast/activitypub/persistence"
|
||||
"github.com/owncast/owncast/activitypub/requests"
|
||||
"github.com/owncast/owncast/core/webhooks"
|
||||
"github.com/owncast/owncast/persistence/configrepository"
|
||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||
webutils "github.com/owncast/owncast/webserver/utils"
|
||||
@@ -36,6 +37,9 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Fire fediverse engagement follow event.
|
||||
go webhooks.SendFediverseEngagementFollowEvent(*approval.ActorIRI)
|
||||
|
||||
configRepository := configrepository.Get()
|
||||
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user