21 changed files with 543 additions and 60 deletions
@ -0,0 +1,71 @@
@@ -0,0 +1,71 @@
|
||||
package moderation |
||||
|
||||
import ( |
||||
"encoding/json" |
||||
"net/http" |
||||
"strings" |
||||
"time" |
||||
|
||||
"github.com/owncast/owncast/controllers" |
||||
"github.com/owncast/owncast/core/chat" |
||||
"github.com/owncast/owncast/core/chat/events" |
||||
"github.com/owncast/owncast/core/user" |
||||
log "github.com/sirupsen/logrus" |
||||
) |
||||
|
||||
// GetUserDetails returns the details of a chat user for moderators.
|
||||
func GetUserDetails(w http.ResponseWriter, r *http.Request) { |
||||
type connectedClient struct { |
||||
MessageCount int `json:"messageCount"` |
||||
UserAgent string `json:"userAgent"` |
||||
ConnectedAt time.Time `json:"connectedAt"` |
||||
Geo string `json:"geo,omitempty"` |
||||
} |
||||
|
||||
type response struct { |
||||
User *user.User `json:"user"` |
||||
ConnectedClients []connectedClient `json:"connectedClients"` |
||||
Messages []events.UserMessageEvent `json:"messages"` |
||||
} |
||||
|
||||
pathComponents := strings.Split(r.URL.Path, "/") |
||||
uid := pathComponents[len(pathComponents)-1] |
||||
|
||||
u := user.GetUserByID(uid) |
||||
|
||||
if u == nil { |
||||
w.WriteHeader(http.StatusNotFound) |
||||
return |
||||
} |
||||
|
||||
c, _ := chat.GetClientsForUser(uid) |
||||
clients := make([]connectedClient, len(c)) |
||||
for i, c := range c { |
||||
client := connectedClient{ |
||||
MessageCount: c.MessageCount, |
||||
UserAgent: c.UserAgent, |
||||
ConnectedAt: c.ConnectedAt, |
||||
} |
||||
if c.Geo != nil { |
||||
client.Geo = c.Geo.CountryCode |
||||
} |
||||
|
||||
clients[i] = client |
||||
} |
||||
|
||||
messages, err := chat.GetMessagesFromUser(uid) |
||||
if err != nil { |
||||
log.Errorln(err) |
||||
} |
||||
|
||||
res := response{ |
||||
User: u, |
||||
ConnectedClients: clients, |
||||
Messages: messages, |
||||
} |
||||
|
||||
w.Header().Set("Content-Type", "application/json") |
||||
if err := json.NewEncoder(w).Encode(res); err != nil { |
||||
controllers.InternalErrorHandler(w, err) |
||||
} |
||||
} |
@ -1,27 +1,179 @@
@@ -1,27 +1,179 @@
|
||||
import { Col, Row } from 'antd'; |
||||
import { Button, Col, Row, Spin } from 'antd'; |
||||
import { useEffect, useState } from 'react'; |
||||
import ChatModeration from '../../../services/moderation-service'; |
||||
import s from './ChatModerationDetailsModal.module.scss'; |
||||
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */ |
||||
interface Props { |
||||
// userID: string;
|
||||
userId: string; |
||||
accessToken: string; |
||||
} |
||||
|
||||
export interface UserDetails { |
||||
user: User; |
||||
connectedClients: Client[]; |
||||
messages: Message[]; |
||||
} |
||||
|
||||
export interface Client { |
||||
messageCount: number; |
||||
userAgent: string; |
||||
connectedAt: Date; |
||||
geo: string; |
||||
} |
||||
|
||||
export interface Message { |
||||
id: string; |
||||
timestamp: Date; |
||||
user: null; |
||||
body: string; |
||||
} |
||||
|
||||
export interface User { |
||||
id: string; |
||||
displayName: string; |
||||
displayColor: number; |
||||
createdAt: Date; |
||||
previousNames: string[]; |
||||
scopes: string[]; |
||||
isBot: boolean; |
||||
authenticated: boolean; |
||||
} |
||||
|
||||
const removeMessage = async (messageId: string, accessToken: string) => { |
||||
try { |
||||
ChatModeration.removeMessage(messageId, accessToken); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
}; |
||||
|
||||
const ValueRow = ({ label, value }: { label: string; value: string }) => ( |
||||
<Row justify="space-around" align="middle"> |
||||
<Col span={12}>{label}</Col> |
||||
<Col span={12}>{value}</Col> |
||||
</Row> |
||||
); |
||||
|
||||
const ChatMessageRow = ({ |
||||
id, |
||||
body, |
||||
accessToken, |
||||
}: { |
||||
id: string; |
||||
body: string; |
||||
accessToken: string; |
||||
}) => ( |
||||
<Row justify="space-around" align="middle"> |
||||
<Col span={18}>{body}</Col> |
||||
<Col> |
||||
<Button onClick={() => removeMessage(id, accessToken)}>X</Button> |
||||
</Col> |
||||
</Row> |
||||
); |
||||
|
||||
const ConnectedClient = ({ client }: { client: Client }) => { |
||||
const { messageCount, userAgent, connectedAt, geo } = client; |
||||
|
||||
return ( |
||||
<div> |
||||
<ValueRow label="Messages Sent" value={`${messageCount}`} /> |
||||
<ValueRow label="Geo" value={geo} /> |
||||
<ValueRow label="Connected At" value={connectedAt.toString()} /> |
||||
<ValueRow label="User Agent" value={userAgent} /> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
// eslint-disable-next-line react/prop-types
|
||||
const UserColorBlock = ({ color }) => { |
||||
const bg = `var(--theme-user-colors-${color})`; |
||||
return ( |
||||
<Row justify="space-around" align="middle"> |
||||
<Col span={12}>Color</Col> |
||||
<Col span={12}> |
||||
<div className={s.colorBlock} style={{ backgroundColor: bg }}> |
||||
{color} |
||||
</div> |
||||
</Col> |
||||
</Row> |
||||
); |
||||
}; |
||||
|
||||
export default function ChatModerationDetailsModal(props: Props) { |
||||
const { userId, accessToken } = props; |
||||
const [userDetails, setUserDetails] = useState<UserDetails | null>(null); |
||||
const [loading, setLoading] = useState(true); |
||||
|
||||
const getDetails = async () => { |
||||
try { |
||||
const response = await (await fetch(`/api/moderation/chat/user/${userId}`)).json(); |
||||
setUserDetails(response); |
||||
setLoading(false); |
||||
} catch (e) { |
||||
console.error(e); |
||||
} |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
getDetails(); |
||||
}, []); |
||||
|
||||
if (!userDetails) { |
||||
return null; |
||||
} |
||||
|
||||
const { user, connectedClients, messages } = userDetails; |
||||
const { displayName, displayColor, createdAt, previousNames, scopes, isBot, authenticated } = |
||||
user; |
||||
|
||||
return ( |
||||
<div className={s.modalContainer}> |
||||
<Row justify="space-around" align="middle"> |
||||
<Col span={12}>User created</Col> |
||||
<Col span={12}>xxxx</Col> |
||||
</Row> |
||||
<Spin spinning={loading}> |
||||
<h1>{displayName}</h1> |
||||
<Row justify="space-around" align="middle"> |
||||
{scopes.map(scope => ( |
||||
<Col>{scope}</Col> |
||||
))} |
||||
{authenticated && <Col>Authenticated</Col>} |
||||
{isBot && <Col>Bot</Col>} |
||||
</Row> |
||||
|
||||
<UserColorBlock color={displayColor} /> |
||||
|
||||
<ValueRow label="User Created" value={createdAt.toString()} /> |
||||
<ValueRow label="Previous Names" value={previousNames.join(',')} /> |
||||
|
||||
<hr /> |
||||
|
||||
<Row justify="space-around" align="middle"> |
||||
<Col span={12}>Previous names</Col> |
||||
<Col span={12}>xxxx</Col> |
||||
</Row> |
||||
<h2>Currently Connected</h2> |
||||
{connectedClients.length > 0 && ( |
||||
<Row gutter={[15, 15]} wrap> |
||||
{connectedClients.map(client => ( |
||||
<Col flex="auto"> |
||||
<ConnectedClient client={client} /> |
||||
</Col> |
||||
))} |
||||
</Row> |
||||
)} |
||||
|
||||
<h1>Recent Chat Messages</h1> |
||||
<hr /> |
||||
{messages.length > 0 && ( |
||||
<div> |
||||
<h1>Recent Chat Messages</h1> |
||||
|
||||
<div className={s.chatHistory} /> |
||||
<div className={s.chatHistory}> |
||||
{messages.map(message => ( |
||||
<ChatMessageRow |
||||
key={message.id} |
||||
id={message.id} |
||||
body={message.body} |
||||
accessToken={accessToken} |
||||
/> |
||||
))} |
||||
</div> |
||||
</div> |
||||
)} |
||||
</Spin> |
||||
</div> |
||||
); |
||||
} |
||||
|
@ -0,0 +1,38 @@
@@ -0,0 +1,38 @@
|
||||
const HIDE_MESSAGE_ENDPOINT = `/api/chat/messagevisibility`; |
||||
const BAN_USER_ENDPOINT = `/api/chat/users/setenabled`; |
||||
|
||||
class ChatModerationService { |
||||
public static async removeMessage(id: string, accessToken: string): Promise<any> { |
||||
const url = new URL(HIDE_MESSAGE_ENDPOINT, window.location.toString()); |
||||
url.searchParams.append('accessToken', accessToken); |
||||
const hideMessageUrl = url.toString(); |
||||
|
||||
const options = { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify({ idArray: [id] }), |
||||
}; |
||||
|
||||
await fetch(hideMessageUrl, options); |
||||
} |
||||
|
||||
public static async banUser(id: string, accessToken: string): Promise<any> { |
||||
const url = new URL(BAN_USER_ENDPOINT, window.location.toString()); |
||||
url.searchParams.append('accessToken', accessToken); |
||||
const hideMessageUrl = url.toString(); |
||||
|
||||
const options = { |
||||
method: 'POST', |
||||
headers: { |
||||
'Content-Type': 'application/json', |
||||
}, |
||||
body: JSON.stringify({ id }), |
||||
}; |
||||
|
||||
await fetch(hideMessageUrl, options); |
||||
} |
||||
} |
||||
|
||||
export default ChatModerationService; |
Loading…
Reference in new issue