* Added ExtensionLoader, a generic plugin system with internal, external assembly, and source file plugin loading as well as interface binding
* Switched Simian over to ExtensionLoader git-svn-id: http://libopenmetaverse.googlecode.com/svn/trunk@2258 52acb1d6-8a22-11de-b505-999d5b087335
This commit is contained in:
@@ -1,150 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using System.CodeDom.Compiler;
|
||||
using System.IO;
|
||||
using OpenMetaverse;
|
||||
|
||||
namespace Simian
|
||||
{
|
||||
public static class ExtensionLoader
|
||||
{
|
||||
/// <summary>
|
||||
/// Exception thrown when there is a problem with an extension
|
||||
/// </summary>
|
||||
public class ExtensionException : Exception
|
||||
{
|
||||
public ExtensionException(string message)
|
||||
: base(message)
|
||||
{
|
||||
}
|
||||
|
||||
public ExtensionException(string message, Exception innerException)
|
||||
: base(message, innerException)
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Currently loaded extensions</summary>
|
||||
public static List<ISimianExtension> Extensions;
|
||||
/// <summary></summary>
|
||||
public static CodeDomProvider CSCompiler;
|
||||
/// <summary></summary>
|
||||
public static CompilerParameters CSCompilerParams;
|
||||
|
||||
static ExtensionLoader()
|
||||
{
|
||||
Extensions = new List<ISimianExtension>();
|
||||
|
||||
CSCompiler = CodeDomProvider.CreateProvider("C#");
|
||||
|
||||
CSCompilerParams = new CompilerParameters();
|
||||
CSCompilerParams.GenerateExecutable = false;
|
||||
CSCompilerParams.GenerateInMemory = true;
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
CSCompilerParams.IncludeDebugInformation = true;
|
||||
else
|
||||
CSCompilerParams.IncludeDebugInformation = false;
|
||||
CSCompilerParams.ReferencedAssemblies.Add("OpenMetaverseTypes.dll");
|
||||
CSCompilerParams.ReferencedAssemblies.Add("OpenMetaverse.dll");
|
||||
CSCompilerParams.ReferencedAssemblies.Add("Simian.exe");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
///
|
||||
/// </summary>
|
||||
/// <param name="path"></param>
|
||||
/// <param name="owner"><seealso cref="Simian"/> server that these extensions belong to</param>
|
||||
public static void LoadAllExtensions(string path, Simian owner)
|
||||
{
|
||||
// Load internal extensions
|
||||
LoadAssemblyExtensions(Assembly.GetExecutingAssembly(), owner);
|
||||
|
||||
// Load extensions from external assemblies
|
||||
List<string> extensionNames = ListExtensionAssemblies(path);
|
||||
foreach (string name in extensionNames)
|
||||
LoadAssemblyExtensions(Assembly.LoadFile(name), owner);
|
||||
|
||||
// Load extensions from external code files
|
||||
extensionNames = ListExtensionSourceFiles(path);
|
||||
foreach (string name in extensionNames)
|
||||
{
|
||||
CompilerResults results = CSCompiler.CompileAssemblyFromFile(CSCompilerParams, name);
|
||||
if (results.Errors.Count == 0)
|
||||
{
|
||||
LoadAssemblyExtensions(results.CompiledAssembly, owner);
|
||||
}
|
||||
else
|
||||
{
|
||||
Logger.Log("Error(s) compiling " + name, Helpers.LogLevel.Error);
|
||||
foreach (CompilerError error in results.Errors)
|
||||
Logger.Log(error.ToString(), Helpers.LogLevel.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static List<string> ListExtensionAssemblies(string path)
|
||||
{
|
||||
List<string> plugins = new List<string>();
|
||||
string[] files = Directory.GetFiles(path, "Simian.*.dll");
|
||||
|
||||
foreach (string f in files)
|
||||
{
|
||||
try
|
||||
{
|
||||
Assembly a = Assembly.LoadFrom(f);
|
||||
System.Type[] types = a.GetTypes();
|
||||
foreach (System.Type type in types)
|
||||
{
|
||||
if (type.GetInterface("ISimianExtension") != null)
|
||||
{
|
||||
plugins.Add(f);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log(String.Format("Unrecognized extension {0}: {1}", f, e.Message),
|
||||
Helpers.LogLevel.Warning, e);
|
||||
}
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static List<string> ListExtensionSourceFiles(string path)
|
||||
{
|
||||
List<string> plugins = new List<string>();
|
||||
string[] files = Directory.GetFiles(path, "Simian.*.cs");
|
||||
|
||||
foreach (string f in files)
|
||||
{
|
||||
if (File.ReadAllText(f).Contains("ISimianExtension"))
|
||||
plugins.Add(f);
|
||||
}
|
||||
|
||||
return plugins;
|
||||
}
|
||||
|
||||
public static void LoadAssemblyExtensions(Assembly assembly, Simian owner)
|
||||
{
|
||||
foreach (Type t in assembly.GetTypes())
|
||||
{
|
||||
try
|
||||
{
|
||||
if (t.GetInterface("ISimianExtension") != null)
|
||||
{
|
||||
ConstructorInfo info = t.GetConstructor(new Type[] { typeof(Simian) });
|
||||
ISimianExtension extension = (ISimianExtension)info.Invoke(new object[] { owner });
|
||||
Extensions.Add(extension);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.Log("LoadAssemblyExtensions(): " + e.Message, Helpers.LogLevel.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class AccountManager : ISimianExtension, IAccountProvider, IPersistable
|
||||
public class AccountManager : IExtension, IAccountProvider, IPersistable
|
||||
{
|
||||
public string StoreName { get { return "Accounts"; } }
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Imaging;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class AssetManager : ISimianExtension, IAssetProvider
|
||||
public class AssetManager : IExtension, IAssetProvider
|
||||
{
|
||||
Simian Server;
|
||||
Dictionary<UUID, Asset> AssetStore = new Dictionary<UUID, Asset>();
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using System;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class AuthFreeForAll : ISimianExtension, IAuthenticationProvider
|
||||
public class AuthFreeForAll : IExtension, IAuthenticationProvider
|
||||
{
|
||||
Simian server;
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
class AvatarManager : ISimianExtension, IAvatarProvider
|
||||
class AvatarManager : IExtension, IAvatarProvider
|
||||
{
|
||||
Simian Server;
|
||||
int currentWearablesSerialNum = -1;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
using ExtensionLoader;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
@@ -7,7 +8,7 @@ using System.Threading;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class CoarseLocationUpdates : ISimianExtension
|
||||
public class CoarseLocationUpdates : IExtension
|
||||
{
|
||||
Simian Server;
|
||||
Timer CoarseLocationTimer;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
using System;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class ConnectionManagement : ISimianExtension
|
||||
public class ConnectionManagement : IExtension
|
||||
{
|
||||
Simian server;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class FriendManager : ISimianExtension
|
||||
public class FriendManager : IExtension
|
||||
{
|
||||
Simian Server;
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.Threading;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Imaging;
|
||||
using OpenMetaverse.Packets;
|
||||
@@ -81,7 +82,7 @@ namespace Simian.Extensions
|
||||
}
|
||||
}
|
||||
|
||||
public class ImageDelivery : ISimianExtension
|
||||
public class ImageDelivery : IExtension
|
||||
{
|
||||
Simian Server;
|
||||
AssetTexture defaultJP2;
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class InventoryManager : ISimianExtension, IInventoryProvider
|
||||
public class InventoryManager : IExtension, IInventoryProvider
|
||||
{
|
||||
Simian Server;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class Messaging : ISimianExtension
|
||||
public class Messaging : IExtension
|
||||
{
|
||||
Simian Server;
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
class Money : ISimianExtension
|
||||
class Money : IExtension
|
||||
{
|
||||
Simian Server;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class Movement : ISimianExtension
|
||||
public class Movement : IExtension
|
||||
{
|
||||
const int UPDATE_ITERATION = 100; //rate in milliseconds to send ObjectUpdate
|
||||
const bool ENVIRONMENT_SOUNDS = true; //collision sounds, splashing, etc
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Rendering;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class ObjectManager : ISimianExtension
|
||||
public class ObjectManager : IExtension
|
||||
{
|
||||
Simian Server;
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class ParcelManager : ISimianExtension, IParcelProvider
|
||||
public class ParcelManager : IExtension, IParcelProvider
|
||||
{
|
||||
Simian server;
|
||||
Dictionary<int, Parcel> parcels = new Dictionary<int, Parcel>();
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Rendering;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class RenderingPluginMesher : ISimianExtension, IMeshingProvider
|
||||
public class RenderingPluginMesher : IExtension, IMeshingProvider
|
||||
{
|
||||
Simian Server;
|
||||
IRendering Renderer;
|
||||
|
||||
@@ -5,13 +5,14 @@ using System.Drawing.Imaging;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Imaging;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class SceneManager : ISimianExtension, ISceneProvider
|
||||
public class SceneManager : IExtension, ISceneProvider
|
||||
{
|
||||
Simian server;
|
||||
DoubleDictionary<uint, UUID, SimulationObject> sceneObjects = new DoubleDictionary<uint, UUID, SimulationObject>();
|
||||
|
||||
@@ -3,6 +3,7 @@ using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Net;
|
||||
using System.Threading;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Packets;
|
||||
|
||||
@@ -67,7 +68,7 @@ namespace Simian
|
||||
}
|
||||
}
|
||||
|
||||
public class UDPManager : ISimianExtension, IUDPProvider
|
||||
public class UDPManager : IExtension, IUDPProvider
|
||||
{
|
||||
Simian Server;
|
||||
UDPServer udpServer;
|
||||
|
||||
@@ -2,12 +2,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Xml;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.StructuredData;
|
||||
|
||||
namespace Simian.Extensions
|
||||
{
|
||||
public class XMLPersistence : ISimianExtension, IPersistenceProvider
|
||||
public class XMLPersistence : IExtension, IPersistenceProvider
|
||||
{
|
||||
Simian server;
|
||||
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
namespace Simian
|
||||
{
|
||||
/// <summary>
|
||||
/// Abstract base for rendering plugins
|
||||
/// </summary>
|
||||
public interface ISimianExtension
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the simulator is initializing
|
||||
/// </summary>
|
||||
void Start();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the simulator is shutting down
|
||||
/// </summary>
|
||||
void Stop();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,8 @@ using System.IO;
|
||||
using System.Text;
|
||||
using System.Xml;
|
||||
using System.Threading;
|
||||
using System.Reflection;
|
||||
using ExtensionLoader;
|
||||
using OpenMetaverse;
|
||||
using OpenMetaverse.Capabilities;
|
||||
using OpenMetaverse.Packets;
|
||||
@@ -54,16 +56,35 @@ namespace Simian
|
||||
|
||||
RegionHandle = Helpers.UIntsToLong(REGION_X, REGION_Y);
|
||||
|
||||
// Load all of the extensions
|
||||
ExtensionLoader.LoadAllExtensions(AppDomain.CurrentDomain.BaseDirectory, this);
|
||||
|
||||
foreach (ISimianExtension extension in ExtensionLoader.Extensions)
|
||||
try
|
||||
{
|
||||
// Assign to an interface if possible
|
||||
TryAssignToInterface(extension);
|
||||
// Load all of the extensions
|
||||
List<string> references = new List<string>();
|
||||
references.Add("OpenMetaverseTypes.dll");
|
||||
references.Add("OpenMetaverse.dll");
|
||||
references.Add("Simian.exe");
|
||||
|
||||
Dictionary<Type, FieldInfo> assignables = GetInterfaces();
|
||||
|
||||
ExtensionLoader<Simian>.LoadAllExtensions(Assembly.GetExecutingAssembly(),
|
||||
AppDomain.CurrentDomain.BaseDirectory, this, references,
|
||||
"Simian.*.dll", "Simian.*.cs", this, assignables);
|
||||
}
|
||||
catch (ExtensionException ex)
|
||||
{
|
||||
Logger.Log("Interface loading failed, shutting down: " + ex.Message, Helpers.LogLevel.Error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (ISimianExtension extension in ExtensionLoader.Extensions)
|
||||
foreach (IExtension extension in ExtensionLoader<Simian>.Extensions)
|
||||
{
|
||||
// Track all of the extensions with persistence
|
||||
if (extension is IPersistable)
|
||||
PersistentExtensions.Add((IPersistable)extension);
|
||||
}
|
||||
|
||||
foreach (IExtension extension in ExtensionLoader<Simian>.Extensions)
|
||||
{
|
||||
// Start persistance providers after all other extensions
|
||||
if (!(extension is IPersistenceProvider))
|
||||
@@ -73,7 +94,7 @@ namespace Simian
|
||||
}
|
||||
}
|
||||
|
||||
foreach (ISimianExtension extension in ExtensionLoader.Extensions)
|
||||
foreach (IExtension extension in ExtensionLoader<Simian>.Extensions)
|
||||
{
|
||||
// Start the persistance provider(s)
|
||||
if (extension is IPersistenceProvider)
|
||||
@@ -83,28 +104,19 @@ namespace Simian
|
||||
}
|
||||
}
|
||||
|
||||
if (!CheckInterfaces())
|
||||
{
|
||||
Logger.Log("Missing interfaces, shutting down", Helpers.LogLevel.Error);
|
||||
Stop();
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
foreach (ISimianExtension extension in ExtensionLoader.Extensions)
|
||||
foreach (IExtension extension in ExtensionLoader<Simian>.Extensions)
|
||||
{
|
||||
// Stop persistance providers first
|
||||
if (extension is IPersistenceProvider)
|
||||
extension.Stop();
|
||||
}
|
||||
|
||||
foreach (ISimianExtension extension in ExtensionLoader.Extensions)
|
||||
foreach (IExtension extension in ExtensionLoader<Simian>.Extensions)
|
||||
{
|
||||
// Stop all other extensions
|
||||
if (!(extension is IPersistenceProvider))
|
||||
@@ -134,56 +146,17 @@ namespace Simian
|
||||
UDP.BroadcastPacket(offline, PacketCategory.State);
|
||||
}
|
||||
|
||||
void TryAssignToInterface(ISimianExtension extension)
|
||||
Dictionary<Type, FieldInfo> GetInterfaces()
|
||||
{
|
||||
if (extension is IAuthenticationProvider)
|
||||
Authentication = (IAuthenticationProvider)extension;
|
||||
else if (extension is IAccountProvider)
|
||||
Accounts = (IAccountProvider)extension;
|
||||
else if (extension is IUDPProvider)
|
||||
UDP = (IUDPProvider)extension;
|
||||
else if (extension is ISceneProvider)
|
||||
Scene = (ISceneProvider)extension;
|
||||
else if (extension is IAssetProvider)
|
||||
Assets = (IAssetProvider)extension;
|
||||
else if (extension is IAvatarProvider)
|
||||
Avatars = (IAvatarProvider)extension;
|
||||
else if (extension is IInventoryProvider)
|
||||
Inventory = (IInventoryProvider)extension;
|
||||
else if (extension is IParcelProvider)
|
||||
Parcels = (IParcelProvider)extension;
|
||||
else if (extension is IMeshingProvider)
|
||||
Mesher = (IMeshingProvider)extension;
|
||||
Dictionary<Type, FieldInfo> interfaces = new Dictionary<Type, FieldInfo>();
|
||||
|
||||
// Track all of the extensions with persistence
|
||||
if (extension is IPersistable)
|
||||
PersistentExtensions.Add((IPersistable)extension);
|
||||
}
|
||||
foreach (FieldInfo field in this.GetType().GetFields())
|
||||
{
|
||||
if (field.FieldType.IsInterface)
|
||||
interfaces.Add(field.FieldType, field);
|
||||
}
|
||||
|
||||
bool CheckInterfaces()
|
||||
{
|
||||
if (Authentication == null)
|
||||
Logger.Log("No IAuthenticationProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Accounts == null)
|
||||
Logger.Log("No IAccountProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (UDP == null)
|
||||
Logger.Log("No IUDPProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Scene == null)
|
||||
Logger.Log("No ISceneProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Assets == null)
|
||||
Logger.Log("No IAssetProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Avatars == null)
|
||||
Logger.Log("No IAvatarProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Inventory == null)
|
||||
Logger.Log("No IInventoryProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Parcels == null)
|
||||
Logger.Log("No IParcelProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else if (Mesher == null)
|
||||
Logger.Log("No IMeshingProvider interface loaded", Helpers.LogLevel.Error);
|
||||
else
|
||||
return true;
|
||||
|
||||
return false;
|
||||
return interfaces;
|
||||
}
|
||||
|
||||
void InitHttpServer(int port, bool ssl)
|
||||
|
||||
Reference in New Issue
Block a user