mirror of
https://github.com/open-webui/open-webui
synced 2024-11-22 08:07:55 +00:00
enh: tools user info
This commit is contained in:
parent
43ffd61aeb
commit
c50b678dce
@ -2,7 +2,7 @@ import time
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.apps.webui.internal.db import Base, get_db
|
from open_webui.apps.webui.internal.db import Base, get_db
|
||||||
from open_webui.apps.webui.models.groups import Groups
|
from open_webui.apps.webui.models.users import Users, UserResponse
|
||||||
|
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||||
@ -57,6 +57,10 @@ class PromptModel(BaseModel):
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
|
|
||||||
|
class PromptUserResponse(PromptModel):
|
||||||
|
user: Optional[UserResponse] = None
|
||||||
|
|
||||||
|
|
||||||
class PromptForm(BaseModel):
|
class PromptForm(BaseModel):
|
||||||
command: str
|
command: str
|
||||||
title: str
|
title: str
|
||||||
@ -97,15 +101,21 @@ class PromptsTable:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_prompts(self) -> list[PromptModel]:
|
def get_prompts(self) -> list[PromptUserResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [
|
return [
|
||||||
PromptModel.model_validate(prompt) for prompt in db.query(Prompt).all()
|
PromptUserResponse.model_validate(
|
||||||
|
{
|
||||||
|
**PromptModel.model_validate(prompt).model_dump(),
|
||||||
|
"user": Users.get_user_by_id(prompt.user_id).model_dump(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for prompt in db.query(Prompt).all()
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_prompts_by_user_id(
|
def get_prompts_by_user_id(
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[PromptModel]:
|
) -> list[PromptUserResponse]:
|
||||||
prompts = self.get_prompts()
|
prompts = self.get_prompts()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -3,7 +3,7 @@ import time
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
|
from open_webui.apps.webui.internal.db import Base, JSONField, get_db
|
||||||
from open_webui.apps.webui.models.users import Users
|
from open_webui.apps.webui.models.users import Users, UserResponse
|
||||||
from open_webui.env import SRC_LOG_LEVELS
|
from open_webui.env import SRC_LOG_LEVELS
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
from sqlalchemy import BigInteger, Column, String, Text, JSON
|
||||||
@ -86,6 +86,10 @@ class ToolResponse(BaseModel):
|
|||||||
created_at: int # timestamp in epoch
|
created_at: int # timestamp in epoch
|
||||||
|
|
||||||
|
|
||||||
|
class ToolUserResponse(ToolResponse):
|
||||||
|
user: Optional[UserResponse] = None
|
||||||
|
|
||||||
|
|
||||||
class ToolForm(BaseModel):
|
class ToolForm(BaseModel):
|
||||||
id: str
|
id: str
|
||||||
name: str
|
name: str
|
||||||
@ -134,13 +138,21 @@ class ToolsTable:
|
|||||||
except Exception:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_tools(self) -> list[ToolModel]:
|
def get_tools(self) -> list[ToolUserResponse]:
|
||||||
with get_db() as db:
|
with get_db() as db:
|
||||||
return [ToolModel.model_validate(tool) for tool in db.query(Tool).all()]
|
return [
|
||||||
|
ToolUserResponse.model_validate(
|
||||||
|
{
|
||||||
|
**ToolModel.model_validate(tool).model_dump(),
|
||||||
|
"user": Users.get_user_by_id(tool.user_id).model_dump(),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
for tool in db.query(Tool).order_by(Tool.updated_at.desc()).all()
|
||||||
|
]
|
||||||
|
|
||||||
def get_tools_by_user_id(
|
def get_tools_by_user_id(
|
||||||
self, user_id: str, permission: str = "write"
|
self, user_id: str, permission: str = "write"
|
||||||
) -> list[ToolModel]:
|
) -> list[ToolUserResponse]:
|
||||||
tools = self.get_tools()
|
tools = self.get_tools()
|
||||||
|
|
||||||
return [
|
return [
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.apps.webui.models.prompts import PromptForm, PromptModel, Prompts
|
from open_webui.apps.webui.models.prompts import (
|
||||||
|
PromptForm,
|
||||||
|
PromptUserResponse,
|
||||||
|
PromptModel,
|
||||||
|
Prompts,
|
||||||
|
)
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
from fastapi import APIRouter, Depends, HTTPException, status, Request
|
||||||
from open_webui.utils.utils import get_admin_user, get_verified_user
|
from open_webui.utils.utils import get_admin_user, get_verified_user
|
||||||
@ -23,7 +28,7 @@ async def get_prompts(user=Depends(get_verified_user)):
|
|||||||
return prompts
|
return prompts
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list", response_model=list[PromptModel])
|
@router.get("/list", response_model=list[PromptUserResponse])
|
||||||
async def get_prompt_list(user=Depends(get_verified_user)):
|
async def get_prompt_list(user=Depends(get_verified_user)):
|
||||||
if user.role == "admin":
|
if user.role == "admin":
|
||||||
prompts = Prompts.get_prompts()
|
prompts = Prompts.get_prompts()
|
||||||
|
@ -2,7 +2,13 @@ import os
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from open_webui.apps.webui.models.tools import ToolForm, ToolModel, ToolResponse, Tools
|
from open_webui.apps.webui.models.tools import (
|
||||||
|
ToolForm,
|
||||||
|
ToolModel,
|
||||||
|
ToolResponse,
|
||||||
|
ToolUserResponse,
|
||||||
|
Tools,
|
||||||
|
)
|
||||||
from open_webui.apps.webui.utils import load_tools_module_by_id, replace_imports
|
from open_webui.apps.webui.utils import load_tools_module_by_id, replace_imports
|
||||||
from open_webui.config import CACHE_DIR, DATA_DIR
|
from open_webui.config import CACHE_DIR, DATA_DIR
|
||||||
from open_webui.constants import ERROR_MESSAGES
|
from open_webui.constants import ERROR_MESSAGES
|
||||||
@ -19,7 +25,7 @@ router = APIRouter()
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=list[ToolResponse])
|
@router.get("/", response_model=list[ToolUserResponse])
|
||||||
async def get_tools(user=Depends(get_verified_user)):
|
async def get_tools(user=Depends(get_verified_user)):
|
||||||
if user.role == "admin":
|
if user.role == "admin":
|
||||||
tools = Tools.get_tools()
|
tools = Tools.get_tools()
|
||||||
@ -33,7 +39,7 @@ async def get_tools(user=Depends(get_verified_user)):
|
|||||||
############################
|
############################
|
||||||
|
|
||||||
|
|
||||||
@router.get("/list", response_model=list[ToolResponse])
|
@router.get("/list", response_model=list[ToolUserResponse])
|
||||||
async def get_tool_list(user=Depends(get_verified_user)):
|
async def get_tool_list(user=Depends(get_verified_user)):
|
||||||
if user.role == "admin":
|
if user.role == "admin":
|
||||||
tools = Tools.get_tools()
|
tools = Tools.get_tools()
|
||||||
|
@ -84,7 +84,7 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col gap-1 my-1.5">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
||||||
{$i18n.t('Knowledge')}
|
{$i18n.t('Knowledge')}
|
||||||
@ -121,10 +121,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-3 mb-5 grid lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
<div class="mb-5 grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-2">
|
||||||
{#each filteredItems as item}
|
{#each filteredItems as item}
|
||||||
<button
|
<button
|
||||||
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 border border-gray-50 dark:border-gray-850 dark:hover:border-gray-800 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
|
class=" flex space-x-4 cursor-pointer text-left w-full px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-850 transition rounded-xl"
|
||||||
on:click={() => {
|
on:click={() => {
|
||||||
if (item?.meta?.document) {
|
if (item?.meta?.document) {
|
||||||
toast.error(
|
toast.error(
|
||||||
@ -163,7 +163,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3 flex justify-between">
|
<div class="mt-3 flex justify-between">
|
||||||
<div class="text-xs">
|
<div class="text-xs text-gray-500">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={item?.user?.email}
|
content={item?.user?.email}
|
||||||
className="flex shrink-0"
|
className="flex shrink-0"
|
||||||
|
@ -196,7 +196,7 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col gap-1 my-1.5">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex items-center md:self-center text-xl font-medium px-0.5">
|
<div class="flex items-center md:self-center text-xl font-medium px-0.5">
|
||||||
{$i18n.t('Models')}
|
{$i18n.t('Models')}
|
||||||
@ -230,14 +230,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class=" my-2 mb-5 grid gap-2 md:grid-cols-2 lg:grid-cols-3" id="model-list">
|
<div class=" my-2 mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3" id="model-list">
|
||||||
{#each filteredModels as model}
|
{#each filteredModels as model}
|
||||||
<div
|
<div
|
||||||
class=" flex flex-col cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
|
class=" flex flex-col cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-lg transition"
|
||||||
id="model-item-{model.id}"
|
id="model-item-{model.id}"
|
||||||
>
|
>
|
||||||
<div class="flex gap-4 mt-1 mb-0.5">
|
<div class="flex gap-4 mt-1 mb-0.5">
|
||||||
<div class=" w-10">
|
<div class=" w-[44px]">
|
||||||
<div
|
<div
|
||||||
class=" rounded-full object-cover {model.is_active
|
class=" rounded-full object-cover {model.is_active
|
||||||
? ''
|
? ''
|
||||||
@ -252,7 +252,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<a
|
<a
|
||||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
class=" flex flex-1 cursor-pointer w-full"
|
||||||
href={`/?models=${encodeURIComponent(model.id)}`}
|
href={`/?models=${encodeURIComponent(model.id)}`}
|
||||||
>
|
>
|
||||||
<div class=" flex-1 self-center {model.is_active ? '' : 'text-gray-500'}">
|
<div class=" flex-1 self-center {model.is_active ? '' : 'text-gray-500'}">
|
||||||
@ -261,7 +261,7 @@
|
|||||||
className=" w-fit"
|
className=" w-fit"
|
||||||
placement="top-start"
|
placement="top-start"
|
||||||
>
|
>
|
||||||
<div class=" font-medium line-clamp-1">{model.name}</div>
|
<div class=" font-semibold line-clamp-1">{model.name}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
<div class="flex gap-1 text-xs overflow-hidden">
|
<div class="flex gap-1 text-xs overflow-hidden">
|
||||||
@ -278,7 +278,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class=" text-xs">
|
<div class=" text-xs mt-1">
|
||||||
<Tooltip content={model?.user?.email} className="flex shrink-0" placement="top-start">
|
<Tooltip content={model?.user?.email} className="flex shrink-0" placement="top-start">
|
||||||
<div class="shrink-0 text-gray-500">
|
<div class="shrink-0 text-gray-500">
|
||||||
{$i18n.t('By {{name}}', {
|
{$i18n.t('By {{name}}', {
|
||||||
|
@ -21,6 +21,8 @@
|
|||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
import Tooltip from '../common/Tooltip.svelte';
|
||||||
|
import { capitalizeFirstLetter } from '$lib/utils';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
let promptsImportInputElement: HTMLInputElement;
|
let promptsImportInputElement: HTMLInputElement;
|
||||||
@ -103,7 +105,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</DeleteConfirmDialog>
|
</DeleteConfirmDialog>
|
||||||
|
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col gap-1 my-1.5">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
||||||
{$i18n.t('Prompts')}
|
{$i18n.t('Prompts')}
|
||||||
@ -137,19 +139,33 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{#each filteredItems as prompt}
|
{#each filteredItems as prompt}
|
||||||
<div
|
<div
|
||||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
|
||||||
>
|
>
|
||||||
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
<div class=" flex flex-1 space-x-4 cursor-pointer w-full">
|
||||||
<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
|
<a href={`/workspace/prompts/edit?command=${encodeURIComponent(prompt.command)}`}>
|
||||||
<div class=" flex-1 self-center pl-1.5">
|
<div class=" flex-1 flex items-center gap-2 self-center">
|
||||||
<div class=" font-semibold line-clamp-1">{prompt.command}</div>
|
<div class=" font-semibold line-clamp-1 capitalize">{prompt.title}</div>
|
||||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||||
{prompt.title}
|
{prompt.command}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class=" text-xs">
|
||||||
|
<Tooltip
|
||||||
|
content={prompt?.user?.email}
|
||||||
|
className="flex shrink-0"
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
<div class="shrink-0 text-gray-500">
|
||||||
|
{$i18n.t('By {{name}}', {
|
||||||
|
name: capitalizeFirstLetter(prompt?.user?.name ?? prompt?.user?.email)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row gap-0.5 self-center">
|
<div class="flex flex-row gap-0.5 self-center">
|
||||||
|
@ -30,6 +30,7 @@
|
|||||||
import Plus from '../icons/Plus.svelte';
|
import Plus from '../icons/Plus.svelte';
|
||||||
import ChevronRight from '../icons/ChevronRight.svelte';
|
import ChevronRight from '../icons/ChevronRight.svelte';
|
||||||
import Spinner from '../common/Spinner.svelte';
|
import Spinner from '../common/Spinner.svelte';
|
||||||
|
import { capitalizeFirstLetter } from '$lib/utils';
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
@ -172,7 +173,7 @@
|
|||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{#if loaded}
|
{#if loaded}
|
||||||
<div class="flex flex-col gap-1 mt-1.5 mb-2">
|
<div class="flex flex-col gap-1 my-1.5">
|
||||||
<div class="flex justify-between items-center">
|
<div class="flex justify-between items-center">
|
||||||
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
<div class="flex md:self-center text-xl font-medium px-0.5 items-center">
|
||||||
{$i18n.t('Tools')}
|
{$i18n.t('Tools')}
|
||||||
@ -206,10 +207,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-5">
|
<div class="mb-5 gap-2 grid lg:grid-cols-2 xl:grid-cols-3">
|
||||||
{#each filteredItems as tool}
|
{#each filteredItems as tool}
|
||||||
<div
|
<div
|
||||||
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl"
|
class=" flex space-x-4 cursor-pointer w-full px-3 py-2 dark:hover:bg-white/5 hover:bg-black/5 rounded-xl transition"
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
class=" flex flex-1 space-x-3.5 cursor-pointer w-full"
|
||||||
@ -217,33 +218,47 @@
|
|||||||
>
|
>
|
||||||
<div class="flex items-center text-left">
|
<div class="flex items-center text-left">
|
||||||
<div class=" flex-1 self-center pl-1">
|
<div class=" flex-1 self-center pl-1">
|
||||||
<div class=" font-semibold flex items-center gap-1.5">
|
<Tooltip content={tool?.meta?.description ?? ''} placement="top-start">
|
||||||
<div
|
<div class=" font-semibold flex items-center gap-1.5">
|
||||||
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
|
||||||
>
|
|
||||||
TOOL
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if tool?.meta?.manifest?.version}
|
|
||||||
<div
|
<div
|
||||||
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
class=" text-xs font-bold px-1 rounded uppercase line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
>
|
>
|
||||||
v{tool?.meta?.manifest?.version ?? ''}
|
TOOL
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="line-clamp-1">
|
{#if tool?.meta?.manifest?.version}
|
||||||
{tool.name}
|
<div
|
||||||
|
class="text-xs font-bold px-1 rounded line-clamp-1 bg-gray-500/20 text-gray-700 dark:text-gray-200"
|
||||||
|
>
|
||||||
|
v{tool?.meta?.manifest?.version ?? ''}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="line-clamp-1">
|
||||||
|
{tool.name}
|
||||||
|
|
||||||
|
<span class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Tooltip>
|
||||||
|
|
||||||
<div class="flex gap-1.5 px-1">
|
|
||||||
<div class=" text-gray-500 text-xs font-medium flex-shrink-0">{tool.id}</div>
|
|
||||||
|
|
||||||
|
<div class="flex gap-1.5 mb-0.5">
|
||||||
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
<div class=" text-xs overflow-hidden text-ellipsis line-clamp-1">
|
||||||
{tool.meta.description}
|
{tool.meta.description}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="text-xs text-gray-500 shrink-0">
|
||||||
|
<Tooltip
|
||||||
|
content={tool?.user?.email}
|
||||||
|
className="flex shrink-0"
|
||||||
|
placement="top-start"
|
||||||
|
>
|
||||||
|
{$i18n.t('By {{name}}', {
|
||||||
|
name: capitalizeFirstLetter(tool?.user?.name ?? tool?.user?.email)
|
||||||
|
})}
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
Loading…
Reference in New Issue
Block a user