mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
webhooks: add "user created" webhook
Webhooks send a POST to an admin-supplied URL when something happens, with relevant information sent in JSON. One has been added for creating users in Settings > Webhooks > User Created. Lazily, the portion of GetUsers which generates a respUser has been factored out, and is called to send the JSON payload. A stripped-down common.Req method has been added, which is used by the barebones WebhookSender struct.
This commit is contained in:
parent
8307d3da90
commit
e5f79c60ae
52
api-users.go
52
api-users.go
@ -42,7 +42,7 @@ func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
|
|||||||
profile = p
|
profile = p
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
nu /*wg*/, _ := app.NewUserPostVerification(NewUserParams{
|
||||||
Req: req,
|
Req: req,
|
||||||
SourceType: ActivityAdmin,
|
SourceType: ActivityAdmin,
|
||||||
Source: gc.GetString("jfId"),
|
Source: gc.GetString("jfId"),
|
||||||
@ -59,6 +59,8 @@ func (app *appContext) NewUserFromAdmin(gc *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
respondUser(nu.Status, nu.Created, welcomeMessageSentIfNecessary, nu.Message, gc)
|
respondUser(nu.Status, nu.Created, welcomeMessageSentIfNecessary, nu.Message, gc)
|
||||||
|
// These don't need to complete anytime soon
|
||||||
|
// wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Creates a new Jellyfin user via invite code
|
// @Summary Creates a new Jellyfin user via invite code
|
||||||
@ -205,8 +207,7 @@ func (app *appContext) NewUserFromInvite(gc *gin.Context) {
|
|||||||
profile = &p
|
profile = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: Use NewUserPostVerification
|
nu /*wg*/, _ := app.NewUserPostVerification(NewUserParams{
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
|
||||||
Req: req,
|
Req: req,
|
||||||
SourceType: sourceType,
|
SourceType: sourceType,
|
||||||
Source: source,
|
Source: source,
|
||||||
@ -341,7 +342,10 @@ func (app *appContext) NewUserFromInvite(gc *gin.Context) {
|
|||||||
code = 400
|
code = 400
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
gc.JSON(code, validation)
|
gc.JSON(code, validation)
|
||||||
|
// These don't need to complete anytime soon
|
||||||
|
// wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Enable/Disable a list of users, optionally notifying them why.
|
// @Summary Enable/Disable a list of users, optionally notifying them why.
|
||||||
@ -822,27 +826,10 @@ func (app *appContext) AdminPasswordReset(gc *gin.Context) {
|
|||||||
respondBool(204, true, gc)
|
respondBool(204, true, gc)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Summary Get a list of Jellyfin users.
|
func (app *appContext) userSummary(jfUser mediabrowser.User) respUser {
|
||||||
// @Produce json
|
|
||||||
// @Success 200 {object} getUsersDTO
|
|
||||||
// @Failure 500 {object} stringResponse
|
|
||||||
// @Router /users [get]
|
|
||||||
// @Security Bearer
|
|
||||||
// @tags Users
|
|
||||||
func (app *appContext) GetUsers(gc *gin.Context) {
|
|
||||||
var resp getUsersDTO
|
|
||||||
users, err := app.jf.GetUsers(false)
|
|
||||||
resp.UserList = make([]respUser, len(users))
|
|
||||||
if err != nil {
|
|
||||||
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
|
||||||
respond(500, "Couldn't get users", gc)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
adminOnly := app.config.Section("ui").Key("admin_only").MustBool(true)
|
||||||
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
allowAll := app.config.Section("ui").Key("allow_all").MustBool(false)
|
||||||
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
|
referralsEnabled := app.config.Section("user_page").Key("referrals").MustBool(false)
|
||||||
i := 0
|
|
||||||
for _, jfUser := range users {
|
|
||||||
user := respUser{
|
user := respUser{
|
||||||
ID: jfUser.ID,
|
ID: jfUser.ID,
|
||||||
Name: jfUser.Name,
|
Name: jfUser.Name,
|
||||||
@ -889,6 +876,29 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
|||||||
user.ReferralsEnabled = true
|
user.ReferralsEnabled = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return user
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Summary Get a list of Jellyfin users.
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} getUsersDTO
|
||||||
|
// @Failure 500 {object} stringResponse
|
||||||
|
// @Router /users [get]
|
||||||
|
// @Security Bearer
|
||||||
|
// @tags Users
|
||||||
|
func (app *appContext) GetUsers(gc *gin.Context) {
|
||||||
|
var resp getUsersDTO
|
||||||
|
users, err := app.jf.GetUsers(false)
|
||||||
|
resp.UserList = make([]respUser, len(users))
|
||||||
|
if err != nil {
|
||||||
|
app.err.Printf(lm.FailedGetUsers, lm.Jellyfin, err)
|
||||||
|
respond(500, "Couldn't get users", gc)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
i := 0
|
||||||
|
for _, jfUser := range users {
|
||||||
|
user := app.userSummary(jfUser)
|
||||||
resp.UserList[i] = user
|
resp.UserList[i] = user
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
|
2
api.go
2
api.go
@ -354,6 +354,8 @@ func (app *appContext) ModifyConfig(gc *gin.Context) {
|
|||||||
tempConfig.Section("telegram").Key("language").SetValue(value.(string))
|
tempConfig.Section("telegram").Key("language").SetValue(value.(string))
|
||||||
} else if app.configBase.Sections[section].Settings[setting].Type == "list" {
|
} else if app.configBase.Sections[section].Settings[setting].Type == "list" {
|
||||||
splitValues := strings.Split(value.(string), "|")
|
splitValues := strings.Split(value.(string), "|")
|
||||||
|
// Delete the key first to get rid of any shadow values
|
||||||
|
tempConfig.Section(section).DeleteKey(setting)
|
||||||
for i, v := range splitValues {
|
for i, v := range splitValues {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
tempConfig.Section(section).Key(setting).SetValue(v)
|
tempConfig.Section(section).Key(setting).SetValue(v)
|
||||||
|
@ -1,10 +1,16 @@
|
|||||||
package common
|
package common
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
lm "github.com/hrfee/jfa-go/logmessages"
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
)
|
)
|
||||||
@ -77,3 +83,68 @@ type ConfigurableTransport interface {
|
|||||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||||
SetTransport(t *http.Transport)
|
SetTransport(t *http.Transport)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stripped down-ish version of rough http request function used in most of the API clients.
|
||||||
|
func Req(httpClient *http.Client, timeoutHandler TimeoutHandler, mode string, uri string, data any, queryParams url.Values, headers map[string]string, response bool) (string, int, error) {
|
||||||
|
var params []byte
|
||||||
|
if data != nil {
|
||||||
|
params, _ = json.Marshal(data)
|
||||||
|
}
|
||||||
|
if qp := queryParams.Encode(); qp != "" {
|
||||||
|
uri += "?" + qp
|
||||||
|
}
|
||||||
|
var req *http.Request
|
||||||
|
if data != nil {
|
||||||
|
req, _ = http.NewRequest(mode, uri, bytes.NewBuffer(params))
|
||||||
|
} else {
|
||||||
|
req, _ = http.NewRequest(mode, uri, nil)
|
||||||
|
}
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
for name, value := range headers {
|
||||||
|
req.Header.Add(name, value)
|
||||||
|
}
|
||||||
|
resp, err := httpClient.Do(req)
|
||||||
|
if resp == nil {
|
||||||
|
return "", 0, err
|
||||||
|
}
|
||||||
|
err = GenericErr(resp.StatusCode, err)
|
||||||
|
if timeoutHandler != nil {
|
||||||
|
defer timeoutHandler()
|
||||||
|
}
|
||||||
|
var responseText string
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if response || err != nil {
|
||||||
|
responseText, err = decodeResp(resp)
|
||||||
|
if err != nil {
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
var msg any
|
||||||
|
err = json.Unmarshal([]byte(responseText), &msg)
|
||||||
|
if err != nil {
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
if msg != nil {
|
||||||
|
err = fmt.Errorf("got %d: %+v", resp.StatusCode, msg)
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
return responseText, resp.StatusCode, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeResp(resp *http.Response) (string, error) {
|
||||||
|
var out io.Reader
|
||||||
|
switch resp.Header.Get("Content-Encoding") {
|
||||||
|
case "gzip":
|
||||||
|
out, _ = gzip.NewReader(resp.Body)
|
||||||
|
default:
|
||||||
|
out = resp.Body
|
||||||
|
}
|
||||||
|
buf := new(strings.Builder)
|
||||||
|
_, err := io.Copy(buf, out)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return buf.String(), nil
|
||||||
|
}
|
||||||
|
@ -2017,6 +2017,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"webhooks": {
|
||||||
|
"order": [],
|
||||||
|
"meta": {
|
||||||
|
"name": "Webhooks",
|
||||||
|
"description": "jfa-go will send a POST request to these URLs when an event occurs, with relevant information. Request information is logged when debug logging is enabled.",
|
||||||
|
"wiki_link": "https://wiki.jfa-go.com/docs/webhooks/"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"created": {
|
||||||
|
"name": "User Created",
|
||||||
|
"required": false,
|
||||||
|
"requires_restart": false,
|
||||||
|
"type": "list",
|
||||||
|
"value": "",
|
||||||
|
"description": "URLs to hit when an account is created through jfa-go. Sends a `respUser` object."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"files": {
|
"files": {
|
||||||
"order": [],
|
"order": [],
|
||||||
"meta": {
|
"meta": {
|
||||||
|
@ -307,6 +307,9 @@ const (
|
|||||||
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
|
CaptchaNotFound = "Captcha \"%s\" not found in invite \"%s\""
|
||||||
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
|
FailedVerifyReCAPTCHA = "Failed to verify reCAPTCHA: %v"
|
||||||
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
|
InvalidHostname = "invalid hostname (wanted \"%s\", got \"%s\")"
|
||||||
|
|
||||||
|
// webhooks.go
|
||||||
|
WebhookRequest = "Webhook request send to \"%s\" (%d): %v"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
9
main.go
9
main.go
@ -121,6 +121,7 @@ type appContext struct {
|
|||||||
version string
|
version string
|
||||||
URLBase, ExternalURI, ExternalDomain string
|
URLBase, ExternalURI, ExternalDomain string
|
||||||
updater *Updater
|
updater *Updater
|
||||||
|
webhooks *WebhookSender
|
||||||
newUpdate bool // Whether whatever's in update is new.
|
newUpdate bool // Whether whatever's in update is new.
|
||||||
tag Tag
|
tag Tag
|
||||||
update Update
|
update Update
|
||||||
@ -556,8 +557,14 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Non-consequential if we don't need it
|
||||||
|
app.webhooks = NewWebhookSender(
|
||||||
|
common.NewTimeoutHandler("Webhook", "?", true),
|
||||||
|
app.debug,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Updater proxy set in config.go, don't worry!
|
||||||
if app.proxyEnabled {
|
if app.proxyEnabled {
|
||||||
app.updater.SetTransport(app.proxyTransport)
|
|
||||||
app.jf.SetTransport(app.proxyTransport)
|
app.jf.SetTransport(app.proxyTransport)
|
||||||
for _, c := range app.thirdPartyServices {
|
for _, c := range app.thirdPartyServices {
|
||||||
c.SetTransport(app.proxyTransport)
|
c.SetTransport(app.proxyTransport)
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
type stringResponse struct {
|
type stringResponse struct {
|
||||||
Response string `json:"response" example:"message"`
|
Response string `json:"response" example:"message"`
|
||||||
|
@ -218,7 +218,7 @@ class DOMList extends DOMInput implements SList {
|
|||||||
const addDummy = () => {
|
const addDummy = () => {
|
||||||
const dummyRow = this.inputRow();
|
const dummyRow = this.inputRow();
|
||||||
const input = dummyRow.querySelector("input") as HTMLInputElement;
|
const input = dummyRow.querySelector("input") as HTMLInputElement;
|
||||||
input.placeholder = window.lang.strings("Add");
|
input.placeholder = window.lang.strings("add");
|
||||||
const onDummyChange = () => {
|
const onDummyChange = () => {
|
||||||
if (!(input.value)) return;
|
if (!(input.value)) return;
|
||||||
addDummy();
|
addDummy();
|
||||||
|
@ -130,11 +130,6 @@ type Updater struct {
|
|||||||
binary string
|
binary string
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
|
||||||
func (ud *Updater) SetTransport(t *http.Transport) {
|
|
||||||
ud.httpClient.Transport = t
|
|
||||||
}
|
|
||||||
|
|
||||||
func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
|
func newUpdater(buildroneURL, namespace, repo, version, commit, buildType string) *Updater {
|
||||||
// fmt.Printf(`Updater intializing with "%s", "%s", "%s", "%s", "%s", "%s"\n`, buildroneURL, namespace, repo, version, commit, buildType)
|
// fmt.Printf(`Updater intializing with "%s", "%s", "%s", "%s", "%s", "%s"\n`, buildroneURL, namespace, repo, version, commit, buildType)
|
||||||
bType := off
|
bType := off
|
||||||
|
18
users.go
18
users.go
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@ -49,7 +50,8 @@ type NewUserData struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
// Called after a new-user-creating route has done pre-steps (veryfing contact methods for example).
|
||||||
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData) {
|
func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData, pendingTasks *sync.WaitGroup) {
|
||||||
|
pendingTasks = &sync.WaitGroup{}
|
||||||
// Some helper functions which will behave as our app.info/error/debug
|
// Some helper functions which will behave as our app.info/error/debug
|
||||||
deferLogInfo := func(s string, args ...any) {
|
deferLogInfo := func(s string, args ...any) {
|
||||||
out.Log = func() {
|
out.Log = func() {
|
||||||
@ -124,7 +126,19 @@ func (app *appContext) NewUserPostVerification(p NewUserParams) (out NewUserData
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Welcome email is sent by each user of this method separately..
|
webhookURIs := app.config.Section("webhooks").Key("created").StringsWithShadows("|")
|
||||||
|
if len(webhookURIs) != 0 {
|
||||||
|
summary := app.userSummary(out.User)
|
||||||
|
for _, uri := range webhookURIs {
|
||||||
|
go func() {
|
||||||
|
pendingTasks.Add(1)
|
||||||
|
app.webhooks.Send(uri, summary)
|
||||||
|
pendingTasks.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Welcome email is sent by each user of this method separately.
|
||||||
|
|
||||||
out.Status = 200
|
out.Status = 200
|
||||||
out.Success = true
|
out.Success = true
|
||||||
|
5
views.go
5
views.go
@ -666,7 +666,7 @@ func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lan
|
|||||||
profile = &p
|
profile = &p
|
||||||
}
|
}
|
||||||
|
|
||||||
nu := app.NewUserPostVerification(NewUserParams{
|
nu /*wg*/, _ := app.NewUserPostVerification(NewUserParams{
|
||||||
Req: req,
|
Req: req,
|
||||||
SourceType: sourceType,
|
SourceType: sourceType,
|
||||||
Source: source,
|
Source: source,
|
||||||
@ -707,6 +707,9 @@ func (app *appContext) NewUserFromConfirmationKey(invite Invite, key string, lan
|
|||||||
delete(invKeys, key)
|
delete(invKeys, key)
|
||||||
app.ConfirmationKeys[invite.Code] = invKeys
|
app.ConfirmationKeys[invite.Code] = invKeys
|
||||||
app.confirmationKeysLock.Unlock()
|
app.confirmationKeysLock.Unlock()
|
||||||
|
|
||||||
|
// These don't need to complete anytime soon
|
||||||
|
// wg.Wait()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
38
webhooks.go
Normal file
38
webhooks.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hrfee/jfa-go/common"
|
||||||
|
"github.com/hrfee/jfa-go/logger"
|
||||||
|
lm "github.com/hrfee/jfa-go/logmessages"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebhookSender struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
timeoutHandler common.TimeoutHandler
|
||||||
|
log *logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTransport sets the http.Transport to use for requests. Can be used to set a proxy.
|
||||||
|
func (ws *WebhookSender) SetTransport(t *http.Transport) {
|
||||||
|
ws.httpClient.Transport = t
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebhookSender(timeoutHandler common.TimeoutHandler, log *logger.Logger) *WebhookSender {
|
||||||
|
return &WebhookSender{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 10 * time.Second,
|
||||||
|
},
|
||||||
|
timeoutHandler: timeoutHandler,
|
||||||
|
log: log,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebhookSender) Send(uri string, payload any) (int, error) {
|
||||||
|
_, status, err := common.Req(ws.httpClient, ws.timeoutHandler, http.MethodPost, uri, payload, url.Values{}, nil, true)
|
||||||
|
ws.log.Printf(lm.WebhookRequest, uri, status, err)
|
||||||
|
return status, err
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user