mirror of
https://github.com/hrfee/jfa-go.git
synced 2026-01-18 16:47:42 +01:00
activity: basic query support
only supports queries of fields actually in Activity. The web UI only directly queries two of these, ID and Time (mistakenly referneced as date in the web ui previously). Later commits will come up with creative ways of dealing with all the other query types.
This commit is contained in:
137
activitysort.go
Normal file
137
activitysort.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
const (
|
||||
ACTIVITY_DEFAULT_SORT_FIELD = "Time"
|
||||
// This will be default anyway, as the default value of a bool field is false.
|
||||
// ACTIVITY_DEFAULT_SORT_ASCENDING = false
|
||||
)
|
||||
|
||||
func activityDTONameToField(field string) string {
|
||||
// Only "ID" and "Time" of these are actually searched by the UI.
|
||||
// We support the rest though for other consumers of the API.
|
||||
switch field {
|
||||
case "id":
|
||||
return "ID"
|
||||
case "type":
|
||||
return "Type"
|
||||
case "user_id":
|
||||
return "UserID"
|
||||
case "username":
|
||||
return "Username"
|
||||
case "source_type":
|
||||
return "SourceType"
|
||||
case "source":
|
||||
return "Source"
|
||||
case "source_username":
|
||||
return "SourceUsername"
|
||||
case "invite_code":
|
||||
return "InviteCode"
|
||||
case "value":
|
||||
return "Value"
|
||||
case "time":
|
||||
return "Time"
|
||||
case "ip":
|
||||
return "IP"
|
||||
}
|
||||
return "unknown"
|
||||
// Only these query types actually search the ActivityDTO data.
|
||||
}
|
||||
|
||||
func activityTypeGetterNameToType(getter string) ActivityType {
|
||||
switch getter {
|
||||
case "accountCreation":
|
||||
return ActivityCreation
|
||||
case "accountDeletion":
|
||||
return ActivityDeletion
|
||||
case "accountDisabled":
|
||||
return ActivityDisabled
|
||||
case "accountEnabled":
|
||||
return ActivityEnabled
|
||||
case "contactLinked":
|
||||
return ActivityContactLinked
|
||||
case "contactUnlinked":
|
||||
return ActivityContactUnlinked
|
||||
case "passwordChange":
|
||||
return ActivityChangePassword
|
||||
case "passwordReset":
|
||||
return ActivityResetPassword
|
||||
case "inviteCreated":
|
||||
return ActivityCreateInvite
|
||||
case "inviteDeleted":
|
||||
return ActivityDeleteInvite
|
||||
}
|
||||
return ActivityUnknown
|
||||
}
|
||||
|
||||
// AsDBQuery returns a mutated "query" filtering for the conditions in "q".
|
||||
func (q QueryDTO) AsDBQuery(query *badgerhold.Query) *badgerhold.Query {
|
||||
if query == nil {
|
||||
query = &badgerhold.Query{}
|
||||
}
|
||||
// Special case for activity type:
|
||||
// In the app, there isn't an "activity:<fieldname>" query, but rather "<~fieldname>:true/false" queries.
|
||||
// For other API consumers, we also handle the former later.
|
||||
activityType := activityTypeGetterNameToType(q.Field)
|
||||
if activityType != ActivityUnknown {
|
||||
criterion := query.And("Type")
|
||||
if q.Operator != EqualOperator {
|
||||
panic(fmt.Errorf("impossible operator for activity type: %v", q.Operator))
|
||||
}
|
||||
if q.Value.(bool) == true {
|
||||
query = criterion.Eq(activityType)
|
||||
} else {
|
||||
query = criterion.Ne(activityType)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
fieldName := activityDTONameToField(q.Field)
|
||||
if fieldName == "unknown" {
|
||||
panic("FIXME: Support all the weird queries of the web UI!")
|
||||
}
|
||||
criterion := query.And(fieldName)
|
||||
|
||||
// FIXME: Deal with dates like we do in usercache.go
|
||||
switch q.Operator {
|
||||
case LesserOperator:
|
||||
query = criterion.Lt(q.Value)
|
||||
case EqualOperator:
|
||||
query = criterion.Eq(q.Value)
|
||||
case GreaterOperator:
|
||||
query = criterion.Gt(q.Value)
|
||||
}
|
||||
return query
|
||||
}
|
||||
|
||||
// ActivityMatchesSearchAsDBBaseQuery returns a base query (which you should then apply other mutations to) matching the search "term" to Activities by searching all fields. Does not search the generated title like the web app.
|
||||
func ActivityMatchesSearchAsDBBaseQuery(terms []string) *badgerhold.Query {
|
||||
subQuery := &badgerhold.Query{}
|
||||
// I don't believe you can just do Where("*"), so instead run for each field.
|
||||
for _, fieldName := range []string{"ID", "Type", "UserID", "Username", "SourceType", "Source", "SourceUsername", "InviteCode", "Value", "IP"} {
|
||||
criterion := badgerhold.Where(fieldName)
|
||||
// No case-insentive Contains method, so we use MatchFunc instead
|
||||
subQuery = subQuery.Or(criterion.MatchFunc(func(ra *badgerhold.RecordAccess) (bool, error) {
|
||||
field := ra.Field()
|
||||
// _, ok := field.(string)
|
||||
// if !ok {
|
||||
// return false, fmt.Errorf("field not string: %s", fieldName)
|
||||
// }
|
||||
lower := strings.ToLower(field.(string))
|
||||
for _, term := range terms {
|
||||
if strings.Contains(lower, term) {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}))
|
||||
}
|
||||
|
||||
return subQuery
|
||||
}
|
||||
@@ -6,32 +6,6 @@ import (
|
||||
"github.com/timshannon/badgerhold/v4"
|
||||
)
|
||||
|
||||
func stringToActivityType(v string) ActivityType {
|
||||
switch v {
|
||||
case "creation":
|
||||
return ActivityCreation
|
||||
case "deletion":
|
||||
return ActivityDeletion
|
||||
case "disabled":
|
||||
return ActivityDisabled
|
||||
case "enabled":
|
||||
return ActivityEnabled
|
||||
case "contactLinked":
|
||||
return ActivityContactLinked
|
||||
case "contactUnlinked":
|
||||
return ActivityContactUnlinked
|
||||
case "changePassword":
|
||||
return ActivityChangePassword
|
||||
case "resetPassword":
|
||||
return ActivityResetPassword
|
||||
case "createInvite":
|
||||
return ActivityCreateInvite
|
||||
case "deleteInvite":
|
||||
return ActivityDeleteInvite
|
||||
}
|
||||
return ActivityUnknown
|
||||
}
|
||||
|
||||
func activityTypeToString(v ActivityType) string {
|
||||
switch v {
|
||||
case ActivityCreation:
|
||||
@@ -58,6 +32,32 @@ func activityTypeToString(v ActivityType) string {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
func stringToActivityType(v string) ActivityType {
|
||||
switch v {
|
||||
case "creation":
|
||||
return ActivityCreation
|
||||
case "deletion":
|
||||
return ActivityDeletion
|
||||
case "disabled":
|
||||
return ActivityDisabled
|
||||
case "enabled":
|
||||
return ActivityEnabled
|
||||
case "contactLinked":
|
||||
return ActivityContactLinked
|
||||
case "contactUnlinked":
|
||||
return ActivityContactUnlinked
|
||||
case "changePassword":
|
||||
return ActivityChangePassword
|
||||
case "resetPassword":
|
||||
return ActivityResetPassword
|
||||
case "createInvite":
|
||||
return ActivityCreateInvite
|
||||
case "deleteInvite":
|
||||
return ActivityDeleteInvite
|
||||
}
|
||||
return ActivityUnknown
|
||||
}
|
||||
|
||||
func stringToActivitySource(v string) ActivitySource {
|
||||
switch v {
|
||||
case "user":
|
||||
@@ -88,33 +88,36 @@ func activitySourceToString(v ActivitySource) string {
|
||||
|
||||
// @Summary Get the requested set of activities, Paginated, filtered and sorted. Is a POST because of some issues I was having, ideally should be a GET.
|
||||
// @Produce json
|
||||
// @Param GetActivitiesDTO body GetActivitiesDTO true "search parameters"
|
||||
// @Param ServerSearchReqDTO body ServerSearchReqDTO true "search parameters"
|
||||
// @Success 200 {object} GetActivitiesRespDTO
|
||||
// @Router /activity [post]
|
||||
// @Security Bearer
|
||||
// @tags Activity
|
||||
func (app *appContext) GetActivities(gc *gin.Context) {
|
||||
req := GetActivitiesDTO{}
|
||||
req := ServerSearchReqDTO{}
|
||||
gc.BindJSON(&req)
|
||||
query := &badgerhold.Query{}
|
||||
activityTypes := make([]any, len(req.Type))
|
||||
for i, v := range req.Type {
|
||||
activityTypes[i] = stringToActivityType(v)
|
||||
}
|
||||
if len(activityTypes) != 0 {
|
||||
query = badgerhold.Where("Type").In(activityTypes...)
|
||||
if req.SortByField == "" {
|
||||
req.SortByField = USER_DEFAULT_SORT_FIELD
|
||||
} else {
|
||||
req.SortByField = activityDTONameToField(req.SortByField)
|
||||
}
|
||||
|
||||
var query *badgerhold.Query
|
||||
if len(req.SearchTerms) != 0 {
|
||||
query = ActivityMatchesSearchAsDBBaseQuery(req.SearchTerms)
|
||||
} else {
|
||||
query = &badgerhold.Query{}
|
||||
}
|
||||
|
||||
for _, q := range req.Queries {
|
||||
query = q.AsDBQuery(query)
|
||||
}
|
||||
|
||||
query = query.SortBy(req.SortByField)
|
||||
if !req.Ascending {
|
||||
query = query.Reverse()
|
||||
}
|
||||
|
||||
query = query.SortBy("Time")
|
||||
|
||||
if req.Limit == 0 {
|
||||
req.Limit = 10
|
||||
}
|
||||
|
||||
query = query.Skip(req.Page * req.Limit).Limit(req.Limit)
|
||||
|
||||
var results []Activity
|
||||
|
||||
@@ -936,14 +936,14 @@ func (app *appContext) GetUsers(gc *gin.Context) {
|
||||
|
||||
// @Summary Get a paginated, searchable list of Jellyfin users.
|
||||
// @Produce json
|
||||
// @Param getUsersReqDTO body getUsersReqDTO true "search / pagination parameters"
|
||||
// @Param ServerSearchReqDTO body ServerSearchReqDTO true "search / pagination parameters"
|
||||
// @Success 200 {object} getUsersDTO
|
||||
// @Failure 500 {object} stringResponse
|
||||
// @Router /users [post]
|
||||
// @Security Bearer
|
||||
// @tags Users
|
||||
func (app *appContext) SearchUsers(gc *gin.Context) {
|
||||
req := getUsersReqDTO{}
|
||||
req := ServerSearchReqDTO{}
|
||||
gc.BindJSON(&req)
|
||||
if req.SortByField == "" {
|
||||
req.SortByField = USER_DEFAULT_SORT_FIELD
|
||||
|
||||
10
models.go
10
models.go
@@ -175,10 +175,6 @@ type PaginatedReqDTO struct {
|
||||
Ascending bool `json:"ascending"`
|
||||
}
|
||||
|
||||
type getUsersReqDTO struct {
|
||||
ServerSearchReqDTO
|
||||
}
|
||||
|
||||
type getUsersDTO struct {
|
||||
UserList []*respUser `json:"users"`
|
||||
LastPage bool `json:"last_page"`
|
||||
@@ -445,12 +441,6 @@ type ActivityDTO struct {
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
type GetActivitiesDTO struct {
|
||||
// "SortByField" ignores, it's always time.
|
||||
PaginatedReqDTO
|
||||
Type []string `json:"type"` // Types of activity to get. Leave blank for all.
|
||||
}
|
||||
|
||||
type GetActivitiesRespDTO struct {
|
||||
PaginatedDTO
|
||||
Activities []ActivityDTO `json:"activities"`
|
||||
|
||||
@@ -74,9 +74,9 @@ const queries = (): { [field: string]: QueryType } => { return {
|
||||
string: true,
|
||||
date: false
|
||||
},
|
||||
"date": {
|
||||
"time": {
|
||||
name: window.lang.strings("date"),
|
||||
getter: "date",
|
||||
getter: "time",
|
||||
bool: false,
|
||||
string: false,
|
||||
date: true
|
||||
|
||||
Reference in New Issue
Block a user