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/requests"
|
||||||
"github.com/owncast/owncast/activitypub/resolvers"
|
"github.com/owncast/owncast/activitypub/resolvers"
|
||||||
"github.com/owncast/owncast/core/chat/events"
|
"github.com/owncast/owncast/core/chat/events"
|
||||||
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
@@ -39,20 +40,19 @@ func handleFollowInboxRequest(c context.Context, activity vocab.ActivityStreamsF
|
|||||||
}
|
}
|
||||||
|
|
||||||
localAccountName := configRepository.GetDefaultFederationUsername()
|
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||||
|
actorReference := activity.GetActivityStreamsActor()
|
||||||
|
object := activity.GetActivityStreamsObject()
|
||||||
|
objectIRI := object.At(0).GetIRI().String()
|
||||||
|
actorIRI := actorReference.At(0).GetIRI().String()
|
||||||
|
|
||||||
if approved {
|
if approved {
|
||||||
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
if err := requests.SendFollowAccept(follow.Inbox, activity, localAccountName); err != nil {
|
||||||
log.Errorln("unable to send follow accept", err)
|
log.Errorln("unable to send follow accept", err)
|
||||||
return 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
|
// If this request is approved and we have not previously sent an action to
|
||||||
// chat due to a previous follow request, then do so.
|
// chat due to a previous follow request, then do so.
|
||||||
hasPreviouslyhandled := true // Default so we don't send anything if it fails.
|
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"
|
UserParted EventType = "USER_PARTED"
|
||||||
// UserNameChanged is the event sent when a chat username change takes place.
|
// UserNameChanged is the event sent when a chat username change takes place.
|
||||||
UserNameChanged EventType = "NAME_CHANGE"
|
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 is the event sent when a chat message's visibility changes.
|
||||||
VisibiltyToggled EventType = "VISIBILITY-UPDATE"
|
VisibiltyToggled EventType = "VISIBILITY-UPDATE"
|
||||||
// PING is a ping message.
|
// PING is a ping message.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ var validEvents = []EventType{
|
|||||||
UserJoined,
|
UserJoined,
|
||||||
UserParted,
|
UserParted,
|
||||||
UserNameChanged,
|
UserNameChanged,
|
||||||
|
FediverseEngagementFollow,
|
||||||
VisibiltyToggled,
|
VisibiltyToggled,
|
||||||
StreamStarted,
|
StreamStarted,
|
||||||
StreamStopped,
|
StreamStopped,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from 'antd';
|
} from 'antd';
|
||||||
import dynamic from 'next/dynamic';
|
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 { useTranslation } from 'next-export-i18n';
|
||||||
import { CREATE_WEBHOOK, DELETE_WEBHOOK, fetchData, WEBHOOKS } from '../../utils/apis';
|
import { CREATE_WEBHOOK, DELETE_WEBHOOK, fetchData, WEBHOOKS } from '../../utils/apis';
|
||||||
import { isValidUrl, DEFAULT_TEXTFIELD_URL_PATTERN } from '../../utils/validators';
|
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 { Translation } from '../../components/ui/Translation/Translation';
|
||||||
|
|
||||||
import { AdminLayout } from '../../components/layouts/AdminLayout';
|
import { AdminLayout } from '../../components/layouts/AdminLayout';
|
||||||
|
import { ServerStatusContext } from '../../utils/server-status-context';
|
||||||
|
|
||||||
const { Title, Paragraph } = Typography;
|
const { Title, Paragraph } = Typography;
|
||||||
|
|
||||||
@@ -34,6 +35,11 @@ const availableEvents = {
|
|||||||
CHAT: { name: 'Chat messages', description: 'When a user sends a chat message', color: 'purple' },
|
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_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' },
|
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_CHANGE: {
|
||||||
name: 'User name changed',
|
name: 'User name changed',
|
||||||
description: 'When a user changes their name',
|
description: 'When a user changes their name',
|
||||||
@@ -79,6 +85,8 @@ const NewWebhookModal = (props: Props) => {
|
|||||||
const [selectedEvents, setSelectedEvents] = useState([]);
|
const [selectedEvents, setSelectedEvents] = useState([]);
|
||||||
const [webhookUrl, setWebhookUrl] = useState('');
|
const [webhookUrl, setWebhookUrl] = useState('');
|
||||||
|
|
||||||
|
const { serverConfig } = useContext(ServerStatusContext);
|
||||||
|
|
||||||
const events = Object.keys(availableEvents).map(key => ({
|
const events = Object.keys(availableEvents).map(key => ({
|
||||||
value: key,
|
value: key,
|
||||||
label: availableEvents[key].description,
|
label: availableEvents[key].description,
|
||||||
@@ -104,7 +112,16 @@ const NewWebhookModal = (props: Props) => {
|
|||||||
disabled: selectedEvents?.length === 0 || !isValidUrl(webhookUrl),
|
disabled: selectedEvents?.length === 0 || !isValidUrl(webhookUrl),
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkboxes = events.map(singleEvent => (
|
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}>
|
<Col span={8} key={singleEvent.value}>
|
||||||
<Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox>
|
<Checkbox value={singleEvent.value}>{singleEvent.label}</Checkbox>
|
||||||
</Col>
|
</Col>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/owncast/owncast/activitypub/persistence"
|
"github.com/owncast/owncast/activitypub/persistence"
|
||||||
"github.com/owncast/owncast/activitypub/requests"
|
"github.com/owncast/owncast/activitypub/requests"
|
||||||
|
"github.com/owncast/owncast/core/webhooks"
|
||||||
"github.com/owncast/owncast/persistence/configrepository"
|
"github.com/owncast/owncast/persistence/configrepository"
|
||||||
"github.com/owncast/owncast/webserver/handlers/generated"
|
"github.com/owncast/owncast/webserver/handlers/generated"
|
||||||
webutils "github.com/owncast/owncast/webserver/utils"
|
webutils "github.com/owncast/owncast/webserver/utils"
|
||||||
@@ -36,6 +37,9 @@ func ApproveFollower(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fire fediverse engagement follow event.
|
||||||
|
go webhooks.SendFediverseEngagementFollowEvent(*approval.ActorIRI)
|
||||||
|
|
||||||
configRepository := configrepository.Get()
|
configRepository := configrepository.Get()
|
||||||
localAccountName := configRepository.GetDefaultFederationUsername()
|
localAccountName := configRepository.GetDefaultFederationUsername()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user