Sync with Tautulli

Reject stats request for inactive users
This commit is contained in:
aunefyren 2023-10-30 13:30:01 +01:00
parent d40a0be97a
commit 245957e1bb
11 changed files with 333 additions and 51 deletions

View file

@ -7,6 +7,7 @@ import (
"log"
"os"
"path/filepath"
"sort"
)
var usersPath, _ = filepath.Abs("./config/users.json")
@ -70,6 +71,10 @@ func GetUsers() (users []models.WrapperrUser, err error) {
return users, err
}
sort.Slice(users, func(i, j int) bool {
return users[i].UserID < users[j].UserID
})
// Return users object
return users, nil
}

View file

@ -171,6 +171,7 @@ func initRouter(config models.WrapperrConfig) *gin.Engine {
admin.POST("/get/cache-statistics", routes.ApiWrapperCacheStatistics)
admin.POST("/get/users", routes.ApiGetUsers)
admin.POST("/get/users/:userId", routes.ApiGetUser)
admin.POST("/sync/users", routes.ApiSyncTautulliUsers)
}
}

View file

@ -85,34 +85,36 @@ type TautulliEntry struct {
type TautulliGetUsersReply struct {
Response struct {
Result string `json:"result"`
Message interface{} `json:"message"`
Data []struct {
RowID int `json:"row_id"`
UserID int `json:"user_id"`
Username string `json:"username"`
FriendlyName string `json:"friendly_name"`
Thumb interface{} `json:"thumb"`
Email string `json:"email"`
IsActive int `json:"is_active"`
IsAdmin int `json:"is_admin"`
IsHomeUser interface{} `json:"is_home_user"`
IsAllowSync interface{} `json:"is_allow_sync"`
IsRestricted interface{} `json:"is_restricted"`
DoNotify int `json:"do_notify"`
KeepHistory int `json:"keep_history"`
AllowGuest int `json:"allow_guest"`
ServerToken interface{} `json:"server_token"`
SharedLibraries interface{} `json:"shared_libraries"`
FilterAll interface{} `json:"filter_all"`
FilterMovies interface{} `json:"filter_movies"`
FilterTv interface{} `json:"filter_tv"`
FilterMusic interface{} `json:"filter_music"`
FilterPhotos interface{} `json:"filter_photos"`
} `json:"data"`
Result string `json:"result"`
Message interface{} `json:"message"`
Data []TautulliUser `json:"data"`
} `json:"response"`
}
type TautulliUser struct {
RowID int `json:"row_id"`
UserID int `json:"user_id"`
Username string `json:"username"`
FriendlyName string `json:"friendly_name"`
Thumb interface{} `json:"thumb"`
Email string `json:"email"`
IsActive int `json:"is_active"`
IsAdmin int `json:"is_admin"`
IsHomeUser interface{} `json:"is_home_user"`
IsAllowSync interface{} `json:"is_allow_sync"`
IsRestricted interface{} `json:"is_restricted"`
DoNotify int `json:"do_notify"`
KeepHistory int `json:"keep_history"`
AllowGuest int `json:"allow_guest"`
ServerToken interface{} `json:"server_token"`
SharedLibraries interface{} `json:"shared_libraries"`
FilterAll interface{} `json:"filter_all"`
FilterMovies interface{} `json:"filter_movies"`
FilterTv interface{} `json:"filter_tv"`
FilterMusic interface{} `json:"filter_music"`
FilterPhotos interface{} `json:"filter_photos"`
}
type TautulliStatusReply struct {
Response struct {
Result string `json:"result"`

View file

@ -7,6 +7,7 @@ type WrapperrUser struct {
User string `json:"user_name"`
UserID int `json:"user_id"`
Email string `json:"user_email"`
Active bool `json:"user_active"`
Wrappings []WrapperrHistoryEntry `json:"wrappings"`
}

View file

@ -1,12 +1,12 @@
package modules
import (
"aunefyren/wrapperr/files"
"aunefyren/wrapperr/models"
"aunefyren/wrapperr/utilities"
"encoding/json"
"errors"
"io"
"io/ioutil"
"log"
"net/http"
"net/url"
@ -85,17 +85,47 @@ func TautulliTestConnection(TautulliPort int, TautulliIP string, TautulliHttps b
return tautulli_status, nil
}
func TautulliGetUserId(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string, PlexUser string) (userID int, userName string, userFriendlyName string, userEmail string, err error) {
func TautulliGetUserId(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string, PlexUser string) (userID int, userName string, userFriendlyName string, userEmail string, userActive bool, err error) {
userID = 0
userName = ""
userEmail = ""
userFriendlyName = ""
userActive = false
err = nil
body_reply, err := TautulliGetUsers(TautulliPort, TautulliIP, TautulliHttps, TautulliRoot, TautulliApiKey)
if err != nil {
log.Println("Failed to get users from Tautulli. Error: " + err.Error())
return userID, userName, userFriendlyName, userEmail, userActive, errors.New("Failed to get users from Tautulli.")
}
for i := 0; i < len(body_reply.Response.Data); i++ {
if body_reply.Response.Data[i].UserID != 0 && (strings.ToLower(body_reply.Response.Data[i].Username) == strings.ToLower(PlexUser) || strings.ToLower(body_reply.Response.Data[i].Email) == strings.ToLower(PlexUser)) {
ActiveInt := body_reply.Response.Data[i].IsActive
if ActiveInt == 0 {
userActive = false
} else if ActiveInt == 1 {
userActive = true
} else {
return userID, userName, userFriendlyName, userEmail, userActive, errors.New("Invalid IsActive state in Tautulli.")
}
return body_reply.Response.Data[i].UserID, body_reply.Response.Data[i].Username, body_reply.Response.Data[i].FriendlyName, body_reply.Response.Data[i].Email, userActive, nil
}
}
log.Println("Could not find any user that matched the given Plex Identity: '" + PlexUser + "'.")
return userID, userName, userEmail, userFriendlyName, userActive, errors.New("Failed to find user.")
}
func TautulliGetUsers(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string) (usersReply models.TautulliGetUsersReply, err error) {
usersReply = models.TautulliGetUsersReply{}
err = nil
url_string, err := utilities.BuildURL(TautulliPort, TautulliIP, TautulliHttps, TautulliRoot)
if err != nil {
log.Println(err)
return userID, userName, userFriendlyName, userEmail, errors.New("Failed to build Tautulli connection URL.")
log.Println("Failed to build Tautulli connection URL. Error: " + err.Error())
return usersReply, errors.New("Failed to build Tautulli connection URL.")
}
url_string = url_string + "api/v2/" + "?apikey=" + TautulliApiKey + "&cmd=get_users"
@ -105,36 +135,28 @@ func TautulliGetUserId(TautulliPort int, TautulliIP string, TautulliHttps bool,
req, err := http.NewRequest("GET", url_string, payload)
if err != nil {
log.Println(err)
return userID, userName, userFriendlyName, userEmail, errors.New("Failed to reach Tautulli server.")
log.Println("Failed to reach Tautulli server. Error: " + err.Error())
return usersReply, errors.New("Failed to reach Tautulli server.")
}
req.Header.Add("Accept", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Println(err)
return userID, userName, userFriendlyName, userEmail, errors.New("Failed to reach Tautulli server.")
log.Println("Failed to reach Tautulli server. Error: " + err.Error())
return usersReply, errors.New("Failed to reach Tautulli server.")
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
var body_reply models.TautulliGetUsersReply
json.Unmarshal(body, &body_reply)
json.Unmarshal(body, &usersReply)
if err != nil {
log.Println(err)
return userID, userName, userFriendlyName, userEmail, errors.New("Failed to parse Tautulli response.")
return usersReply, errors.New("Failed to parse Tautulli response.")
}
for i := 0; i < len(body_reply.Response.Data); i++ {
if body_reply.Response.Data[i].UserID != 0 && (strings.ToLower(body_reply.Response.Data[i].Username) == strings.ToLower(PlexUser) || strings.ToLower(body_reply.Response.Data[i].Email) == strings.ToLower(PlexUser)) {
return body_reply.Response.Data[i].UserID, body_reply.Response.Data[i].Username, body_reply.Response.Data[i].FriendlyName, body_reply.Response.Data[i].Email, nil
}
}
log.Println("Could not find any user that matched the given Plex Identity: '" + PlexUser + "'.")
return userID, userName, userEmail, userFriendlyName, errors.New("Failed to find user.")
return usersReply, err
}
func TautulliDownloadStatistics(TautulliPort int, TautulliIP string, TautulliHttps bool, TautulliRoot string, TautulliApiKey string, TautulliLength int, Libraries string, Grouping string, StartDate string) ([]models.TautulliHistoryItem, error) {
@ -165,7 +187,7 @@ func TautulliDownloadStatistics(TautulliPort int, TautulliIP string, TautulliHtt
}
defer res.Body.Close()
body, err := ioutil.ReadAll(res.Body)
body, err := io.ReadAll(res.Body)
var body_reply models.TautulliGetHistoryReply
json.Unmarshal(body, &body_reply)
@ -177,3 +199,53 @@ func TautulliDownloadStatistics(TautulliPort int, TautulliIP string, TautulliHtt
return body_reply.Response.Data.Data, nil
}
func TautulliTestEveryServer() (err error) {
err = nil
config, err := files.GetConfig()
if err != nil {
log.Println("Failed to load Wrapperr configuration. Error: " + err.Error())
return errors.New("Failed to load Wrapperr configuration.")
}
for i := 0; i < len(config.TautulliConfig); i++ {
log.Println("Checking Tautulli server '" + config.TautulliConfig[i].TautulliName + "'.")
tautulli_state, err := TautulliTestConnection(config.TautulliConfig[i].TautulliPort, config.TautulliConfig[i].TautulliIP, config.TautulliConfig[i].TautulliHttps, config.TautulliConfig[i].TautulliRoot, config.TautulliConfig[i].TautulliApiKey)
if err != nil {
log.Println("Failed to reach Tautulli server '" + config.TautulliConfig[i].TautulliName + "'. Error: " + err.Error())
return errors.New("Failed to reach Tautulli server '" + config.TautulliConfig[i].TautulliName + "'.")
} else if !tautulli_state {
log.Println("Failed to ping Tautulli server '" + config.TautulliConfig[i].TautulliName + "' before retrieving statistics.")
return errors.New("Failed to ping Tautulli server '" + config.TautulliConfig[i].TautulliName + "'.")
}
}
return
}
func TautulliGetUsersFromEveryServer() (tautulliUsers []models.TautulliUser, err error) {
err = nil
tautulliUsers = []models.TautulliUser{}
config, err := files.GetConfig()
if err != nil {
log.Println("Failed to load Wrapperr configuration. Error: " + err.Error())
return tautulliUsers, errors.New("Failed to load Wrapperr configuration.")
}
for i := 0; i < len(config.TautulliConfig); i++ {
log.Println("Getting users from Tautulli server '" + config.TautulliConfig[i].TautulliName + "'.")
tautulliReply, err := TautulliGetUsers(config.TautulliConfig[i].TautulliPort, config.TautulliConfig[i].TautulliIP, config.TautulliConfig[i].TautulliHttps, config.TautulliConfig[i].TautulliRoot, config.TautulliConfig[i].TautulliApiKey)
if err != nil {
log.Println("Failed to get users from Tautulli. Error: " + err.Error())
return tautulliUsers, errors.New("Failed to get users from Tautulli.")
}
for _, user := range tautulliReply.Response.Data {
tautulliUsers = append(tautulliUsers, user)
}
}
return
}

View file

@ -58,3 +58,48 @@ func UsersGetUser(userID int) (user models.WrapperrUser, err error) {
return user, errors.New("User not found.")
}
func UsersUpdateUser(userID int, FriendlyName string, userName string, Email string, ActiveInt int) (err error) {
err = nil
Active := false
if ActiveInt == 1 {
Active = true
} else if ActiveInt == 0 {
Active = false
} else {
return errors.New("Invalid active state found.")
}
users, err := files.GetUsers()
if err != nil {
log.Println("Failed to get users. Error: " + err.Error())
return errors.New("Failed to get users.")
}
userFound := false
userIndex := 0
for index, foundUser := range users {
if foundUser.UserID == userID {
userFound = true
userIndex = index
}
}
if !userFound {
return errors.New("Failed to find user based on ID.")
}
users[userIndex].Active = Active
users[userIndex].Email = Email
users[userIndex].FriendlyName = FriendlyName
users[userIndex].User = userName
err = files.SaveUsers(users)
if err != nil {
log.Println("Failed to save users. Error: " + err.Error())
return errors.New("Failed to save users.")
}
return
}

View file

@ -431,3 +431,73 @@ func ApiGetUser(context *gin.Context) {
context.JSON(http.StatusOK, gin.H{"message": "Users recieved.", "data": user})
return
}
func ApiSyncTautulliUsers(context *gin.Context) {
configBool, err := files.GetConfigState()
if err != nil {
log.Println("Failed to retrieve configuration state. Error: " + err.Error())
context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve configuration state."})
context.Abort()
return
} else if !configBool {
context.JSON(http.StatusBadRequest, gin.H{"error": "Wrapperr is not configured."})
context.Abort()
return
}
err = modules.TautulliTestEveryServer()
if err != nil {
log.Println("Failed to test Tautulli server. Error: " + err.Error())
context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to test Tautulli server."})
context.Abort()
return
}
users, err := modules.TautulliGetUsersFromEveryServer()
if err != nil {
log.Println("Failed to get users. Error: " + err.Error())
context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get users."})
context.Abort()
return
}
for _, user := range users {
wrapperrUser, err := modules.UsersGetUser(user.UserID)
if err != nil {
wrapperrUser = models.WrapperrUser{
FriendlyName: user.FriendlyName,
User: user.Username,
UserID: user.UserID,
Email: user.Email,
Wrappings: []models.WrapperrHistoryEntry{},
}
err = modules.UsersSaveUserEntry(wrapperrUser)
if err != nil {
log.Println("Failed to save new user. Error: " + err.Error())
context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to save new user."})
context.Abort()
return
}
} else {
err = modules.UsersUpdateUser(user.UserID, user.FriendlyName, user.Username, user.Email, user.IsActive)
if err != nil {
log.Println("Failed to update user. Error: " + err.Error())
context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user."})
context.Abort()
return
}
}
}
newusers, err := files.GetUsers()
if err != nil {
log.Println("Failed to new users. Error: " + err.Error())
context.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to new users."})
context.Abort()
return
}
context.JSON(http.StatusOK, gin.H{"message": "Users synced.", "data": newusers})
return
}

View file

@ -184,6 +184,7 @@ func ApiWrapperGetStatistics(context *gin.Context) {
var userId int = 0
var userEmail string = ""
var userFriendlyName = ""
var userActive = false
// Try to authorize bearer token from header
authorizationHeader := context.GetHeader("Authorization")
@ -219,11 +220,12 @@ func ApiWrapperGetStatistics(context *gin.Context) {
// Check for friendly name using Tautulli
for i := 0; i < len(config.TautulliConfig); i++ {
_, new_username, new_friendlyname, _, err := modules.TautulliGetUserId(config.TautulliConfig[i].TautulliPort, config.TautulliConfig[i].TautulliIP, config.TautulliConfig[i].TautulliHttps, config.TautulliConfig[i].TautulliRoot, config.TautulliConfig[i].TautulliApiKey, userName)
_, new_username, new_friendlyname, _, new_active, err := modules.TautulliGetUserId(config.TautulliConfig[i].TautulliPort, config.TautulliConfig[i].TautulliIP, config.TautulliConfig[i].TautulliHttps, config.TautulliConfig[i].TautulliRoot, config.TautulliConfig[i].TautulliApiKey, userName)
if err == nil {
userName = new_username
userFriendlyName = new_friendlyname
userActive = new_active
}
break
}
@ -252,7 +254,7 @@ func ApiWrapperGetStatistics(context *gin.Context) {
UserNameFound := false
for i := 0; i < len(config.TautulliConfig); i++ {
new_id, new_username, user_friendlyname, new_email, err := modules.TautulliGetUserId(config.TautulliConfig[i].TautulliPort, config.TautulliConfig[i].TautulliIP, config.TautulliConfig[i].TautulliHttps, config.TautulliConfig[i].TautulliRoot, config.TautulliConfig[i].TautulliApiKey, wrapperr_request.PlexIdentity)
new_id, new_username, user_friendlyname, new_email, new_active, err := modules.TautulliGetUserId(config.TautulliConfig[i].TautulliPort, config.TautulliConfig[i].TautulliIP, config.TautulliConfig[i].TautulliHttps, config.TautulliConfig[i].TautulliRoot, config.TautulliConfig[i].TautulliApiKey, wrapperr_request.PlexIdentity)
if err == nil {
UserNameFound = true
@ -260,6 +262,7 @@ func ApiWrapperGetStatistics(context *gin.Context) {
userId = new_id
userEmail = new_email
userFriendlyName = user_friendlyname
userActive = new_active
}
}
@ -279,6 +282,12 @@ func ApiWrapperGetStatistics(context *gin.Context) {
return
}
if !userActive {
context.JSON(http.StatusUnauthorized, gin.H{"error": "User is not active."})
context.Abort()
return
}
wrapperrReply, _, err := modules.GetWrapperStatistics(userName, userFriendlyName, userId, userEmail, config, adminConfig, false, 0)
if err != nil {
log.Println("Failed to get statistics. Error: " + err.Error())

View file

@ -344,7 +344,30 @@ a {
align-items: center;
}
.user-userid, .user-username, .user-friendlyname, .user-email {
.user-userid, .user-active {
margin: 0.25em 0.5em;
text-align: center;
width: 5em;
overflow: hidden;
}
.user-active-true {
background-color: var(--green);
border-radius: 0.25em;
width: min-content;
padding: 0 0.5em;
margin: auto;
}
.user-active-false {
background-color: var(--red);
border-radius: 0.25em;
width: min-content;
padding: 0 0.5em;
margin: auto;
}
.user-username, .user-friendlyname, .user-email {
margin: 0.25em 0.5em;
text-align: center;
width: 15em;
@ -397,6 +420,14 @@ a {
text-decoration: underline;
}
.user-header-short {
margin: 0.25em 0.5em;
text-align: center;
width: 5em;
overflow: hidden;
text-decoration: underline;
}
.user-logbutton {
width: 4em;
}

View file

@ -14,7 +14,7 @@ function get_stats() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4 && (this.status == 200 || this.status == 400 || this.status == 500)) {
if (this.readyState == 4) {
try {
var result= JSON.parse(this.responseText);
} catch(error) {

View file

@ -16,6 +16,10 @@ function loadAdminPage() {
<hr>
</div>
<div class="form-group newline">
<button class="form-control btn" type="submit" name="tautulliSyncButton" id="tautulliSyncButton" onclick="syncTautulli();"><img src="${root}/assets/synchronize.svg" class="btn_logo"></img><p2 id="tautulliSyncButtonImage">Sync with Tautulli</p2></button>
</div>
<div class='form-group newline'>
<h3>
Wrapperr users
@ -62,12 +66,13 @@ function getUsers() {
function placeUsers(usersArray) {
userModule = document.getElementById("users-module")
userModule.innerHTML += `
userModule.innerHTML = `
<div class="user-headers">
<div class="user-header">ID</div>
<div class="user-header-short">ID</div>
<div class="user-header">Username</div>
<div class="user-header">Friendly name</div>
<div class="user-header">Email</div>
<div class="user-header-short">Active</div>
</div>
`;
@ -81,6 +86,13 @@ function placeUsers(usersArray) {
`;
}
var active_state_class = ""
if(user.user_active) {
active_state_class = "user-active-true"
} else {
active_state_class = "user-active-false"
}
var html = `
<div class="user-object">
<div class="user-details">
@ -88,6 +100,11 @@ function placeUsers(usersArray) {
<div class="user-username">${user.user_name}</div>
<div class="user-friendlyname">${user.user_friendly_name}</div>
<div class="user-email">${user.user_email}</div>
<div class="user-active">
<div class="${active_state_class}">
${user.user_active}
</div>
</div>
</div>
${historyDiv}
@ -150,4 +167,33 @@ function closeModal() {
var modalContent = document.getElementById("modal-content-real");
modal.style.display = "none";
modalContent.innerHTML = "";
}
function syncTautulli() {
var xhttp = new XMLHttpRequest();
xhttp.onreadystatechange = function() {
if (this.readyState == 4) {
try {
var result= JSON.parse(this.responseText);
} catch(error) {
alert('Failed to parse API response.');
console.log('Failed to parse API response. Error: ' + this.responseText);
return;
}
if(result.error) {
alert(result.error);
} else {
placeUsers(result.data);
alert(result.message)
}
}
};
xhttp.withCredentials = true;
xhttp.open("post", api_url + "sync/users");
xhttp.setRequestHeader("Content-Type", "application/json;charset=UTF-8");
xhttp.setRequestHeader("Authorization", cookie);
xhttp.send();
return;
}