在 `MainWindow.xaml.cs` 中,调整了消息对齐逻辑,新增 `SenderColor` 属性以根据用户 ID 设置消息颜色。发送者为当前用户时,消息右对齐并显示蓝色;否则左对齐并显示黑色。 在 `Program.cs` 中,移除了对 HTTP 请求的处理逻辑,简化了客户端连接处理。同时,更新了日志记录,确保准确反映压缩数据的实际长度。
535 lines
29 KiB
C#
535 lines
29 KiB
C#
using System.Net.Sockets;
|
||
using System.Net.Http;
|
||
using log4net;
|
||
using log4net.Config;
|
||
using System.Net;
|
||
using System.Data.SQLite;
|
||
using chatserver.Data;
|
||
using System.Text.Json;
|
||
using System.Reflection;
|
||
using static log4net.Appender.FileAppender;
|
||
using System.IO.Compression;
|
||
using System.Text;
|
||
|
||
[assembly: XmlConfigurator(ConfigFile = "config/log4net.config", Watch = true)]
|
||
namespace chatserver
|
||
{
|
||
internal class ChatServer
|
||
{
|
||
private static readonly ILog log = LogManager.GetLogger(typeof(ChatServer));
|
||
private static readonly object Client_lock = new object();
|
||
private static List<Socket> Client = new();
|
||
private static List<User> LoginUser = new();
|
||
private static Socket? Server;
|
||
private static SQLiteConnection? User_db;
|
||
|
||
static void Main(string[] args)
|
||
{
|
||
//XmlConfigurator.Configure();
|
||
log.Info("Hello World!");
|
||
Server = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
|
||
Server.Bind(new IPEndPoint(IPAddress.Any, 52006));
|
||
Server.Listen(20);
|
||
OpenUser_db();
|
||
log.Info("服务器以在52006端口监听");
|
||
while (true)
|
||
{
|
||
try
|
||
{
|
||
Socket client = Server.Accept();
|
||
lock (Client_lock) { Client.Add(client); }
|
||
log.Info($"用户 {client.RemoteEndPoint} 连接");
|
||
Thread thread = new Thread(() => HandleClient(client));
|
||
thread.Start();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
log.Error("Error accepting client connection: " + ex.Message);
|
||
}
|
||
}
|
||
}
|
||
public static void OpenUser_db()
|
||
{
|
||
log.Info("正在打开数据库连接...");
|
||
User_db = new SQLiteConnection("Data Source=ServerUser.db;Version=3;");
|
||
User_db.Open();
|
||
InitializeTable();
|
||
log.Info("数据库连接已打开");
|
||
}
|
||
static void HandleClient(Socket socket)
|
||
{
|
||
try
|
||
{
|
||
while (true)
|
||
{
|
||
const int prefixSize = sizeof(int);
|
||
byte[] prefixBuffer = new byte[prefixSize];
|
||
int received = socket.Receive(prefixBuffer, 0, 4, SocketFlags.Peek);
|
||
if (received == 4)
|
||
{
|
||
string httpStart = Encoding.ASCII.GetString(prefixBuffer);
|
||
if (httpStart.StartsWith("GET") || httpStart.StartsWith("POST") ||
|
||
httpStart.StartsWith("HEAD") || httpStart.StartsWith("HTTP"))
|
||
{
|
||
log.Warn("检测到HTTP请求,拒绝连接");
|
||
return;
|
||
}
|
||
}
|
||
//读取长度前缀
|
||
int prefixBytesRead = 0;
|
||
while (prefixBytesRead < prefixSize)
|
||
{
|
||
int bytesRead = socket.Receive(prefixBuffer, prefixBytesRead, prefixSize - prefixBytesRead, SocketFlags.None);
|
||
if (bytesRead == 0)
|
||
return;
|
||
prefixBytesRead += bytesRead;
|
||
}
|
||
//解析消息长度
|
||
int messageLength = BitConverter.ToInt32(prefixBuffer, 0);
|
||
if (messageLength <= 0 || messageLength > 10 * 1024 * 1024)
|
||
{
|
||
log.Error($"无效消息长度: {messageLength}");
|
||
continue;
|
||
}
|
||
//读取完整消息
|
||
byte[] messageBuffer = new byte[messageLength];
|
||
int totalBytesRead = 0;
|
||
while (totalBytesRead < messageLength)
|
||
{
|
||
int bytesRead = socket.Receive(messageBuffer, totalBytesRead, messageLength - totalBytesRead, SocketFlags.None);
|
||
if (bytesRead == 0)
|
||
return;
|
||
totalBytesRead += bytesRead;
|
||
}
|
||
//解压缩
|
||
byte[] decompressedData = Decompress(messageBuffer);
|
||
string message = System.Text.Encoding.UTF8.GetString(decompressedData);
|
||
//处理信息
|
||
log.Info("Received message: " + message);
|
||
var Type = JsonSerializer.Deserialize<TypeData>(message);
|
||
if (Type != null)
|
||
{
|
||
if (Type.type == "register")
|
||
{
|
||
var sginuser = JsonSerializer.Deserialize<SignData>(message);
|
||
if (sginuser != null && sginuser.username != null && sginuser.password != null)
|
||
{
|
||
log.Info($"用户 {sginuser.username} 正在注册");
|
||
if (string.IsNullOrEmpty(sginuser.username) || string.IsNullOrEmpty(sginuser.password))
|
||
{
|
||
log.Warn("用户名或密码不能为空");
|
||
var emptyResult = new SignResultData { type = "register", status = "error_-1", message = "用户名或密码不能为空" };
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(emptyResult));
|
||
continue; // 如果用户名或密码为空,则跳过注册流程
|
||
}
|
||
if (sginuser.username.Length < 2 || sginuser.username.Length > 20)
|
||
{
|
||
log.Warn($"用户注册时 {sginuser.username} 用户名长度不符合要求");
|
||
var lengthResult = new SignResultData { type = "register", status = "error_2", message = "用户名长度必须在2到20个字符之间" };
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(lengthResult));
|
||
continue; // 如果用户名长度不符合要求,则跳过注册流程
|
||
}
|
||
if (sginuser.password.Length < 4 || sginuser.password.Length > 20)
|
||
{
|
||
log.Warn($"用户注册时 {sginuser.username} 密码长度不符合要求");
|
||
var weakPwdResult = new SignResultData { type = "register", status = "error_1", message = "密码长度必须在4到20个字符之间" };
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult));
|
||
continue; // 如果密码过弱,则跳过注册流程
|
||
}
|
||
if (UserExists(sginuser.username))
|
||
{
|
||
log.Warn($"用户 {sginuser.username} 已存在");
|
||
var Result = new SignResultData { type = "register", status = "error_0", message = "用户名已存在" };
|
||
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(Result)));
|
||
continue;// 如果用户名已存在,则跳过注册流程
|
||
}
|
||
var cmd = new SQLiteCommand("INSERT INTO users (userid, username, password) VALUES (@userid, @username, @password)", User_db);
|
||
var timedUlid = Ulid.NewUlid(DateTimeOffset.UtcNow);
|
||
cmd.Parameters.AddWithValue("@userid", timedUlid);
|
||
cmd.Parameters.AddWithValue("@username", sginuser.username);
|
||
cmd.Parameters.AddWithValue("@password", sginuser.password);
|
||
cmd.ExecuteNonQuery();
|
||
log.Info($"用户 {sginuser.username} 注册成功(id:{timedUlid})");
|
||
var result = new SignResultData { type = "register", status = "succeed", message = "注册成功" };
|
||
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)));
|
||
}
|
||
}
|
||
else if (Type.type == "login")
|
||
{
|
||
var loginData = JsonSerializer.Deserialize<LoginData>(message);
|
||
if (loginData != null)
|
||
{
|
||
if (string.IsNullOrEmpty(loginData.username) || string.IsNullOrEmpty(loginData.password))
|
||
{
|
||
log.Warn("用户名或密码不能为空");
|
||
var emptyResult = new LoginResultData { type = "login", status = "error_-1", message = "用户名或密码不能为空" };
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(emptyResult));
|
||
continue;
|
||
}
|
||
if (loginData.username.Length < 2 || loginData.username.Length > 20)
|
||
{
|
||
log.Warn($"用户登录时 {loginData.username} 用户名长度不符合要求");
|
||
var lengthResult = new LoginResultData { type = "login", status = "error_2", message = "用户名长度必须在2到20个字符之间" };
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(lengthResult));
|
||
continue;
|
||
}
|
||
if (loginData.password.Length > 20)
|
||
{
|
||
log.Warn($"用户登录时 {loginData.username} 密码长度不符合要求");
|
||
var weakPwdResult = new LoginResultData { type = "login", status = "error_1", message = "密码长度不能超过20个字符" };
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(weakPwdResult));
|
||
continue;
|
||
}
|
||
var Authentication = UserAuthentication(loginData.username, loginData.password);
|
||
if (Authentication != "")
|
||
{
|
||
log.Info($"用户 {loginData.username} 登录成功 (id:{Authentication})");
|
||
var timedUlid = Ulid.NewUlid(DateTimeOffset.UtcNow);
|
||
lock (Client_lock)
|
||
{
|
||
LoginUser.Add(new User
|
||
{
|
||
UserId = Authentication,
|
||
LoginIP = socket.RemoteEndPoint?.ToString() ?? "Unknown:0",
|
||
Avatar = null,
|
||
token = timedUlid.ToString()
|
||
});
|
||
}
|
||
var result = new LoginResultData
|
||
{
|
||
type = "login",
|
||
status = "succeed",
|
||
message = "登录成功",
|
||
token = timedUlid.ToString(),
|
||
username = loginData.username,
|
||
userid = Authentication
|
||
};
|
||
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)));
|
||
}
|
||
else
|
||
{
|
||
log.Warn($"用户 {loginData.username} 登录失败,用户名或密码错误");
|
||
var result = new LoginResultData { type = "login", status = "error_0", message = "用户名或密码错误" };
|
||
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(result)));
|
||
}
|
||
}
|
||
}
|
||
else if (Type.type == "chat")
|
||
{
|
||
var chatData = JsonSerializer.Deserialize<ChatData>(message);
|
||
if (chatData != null && chatData.message != null)
|
||
{
|
||
log.Info($"接收到聊天消息(长度: {chatData.message.Length} )");
|
||
if (Client.Count == 0)
|
||
{
|
||
log.Warn("没有客户端连接,取消发送消息。");
|
||
return;
|
||
}
|
||
// 获取发送者完整端点(IP: 端口)
|
||
string senderEndpoint = socket.RemoteEndPoint?.ToString() ?? "Unknown:0";
|
||
// ==== 修改:使用完整端点验证(非IP部分)====
|
||
var loginUser = LoginUser.FirstOrDefault(u => u.LoginIP == senderEndpoint && u.UserId == chatData.userid);
|
||
if (loginUser == null || loginUser.token != chatData.token)
|
||
{
|
||
log.Warn($"聊天消息验证失败:端点 {senderEndpoint} 未登录或 token 不匹配");
|
||
var errorData = new ChatRegisterData
|
||
{
|
||
type = "chat",
|
||
userid = chatData.userid ?? "Unid",
|
||
status = "error_0",
|
||
message = "未登录或token无效,无法发送消息",
|
||
msgtype = MessageType.Text,
|
||
timestamp = DateTime.Now
|
||
};
|
||
SendWithPrefix(socket,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(errorData)));
|
||
continue;
|
||
}
|
||
|
||
List<Socket> clientsCopy;
|
||
List<User> usersCopy;
|
||
// 获取集合快照
|
||
lock (Client_lock)
|
||
{
|
||
clientsCopy = new List<Socket>(Client); // 创建客户端列表副本
|
||
usersCopy = new List<User>(LoginUser); // 创建用户列表副本
|
||
}
|
||
var chatRegisterData = new ChatRegisterData
|
||
{
|
||
type = "chat",
|
||
userid = chatData.userid ?? "Unid",
|
||
user = GetUsernameByUserId(chatData.userid!),
|
||
message = chatData.message,
|
||
msgtype = chatData.msgtype ?? MessageType.Text,
|
||
status = "succeed",
|
||
timestamp = DateTime.Now
|
||
};
|
||
Task.Run(() =>
|
||
{
|
||
try
|
||
{
|
||
using (var dbConnection = new SQLiteConnection("Data Source=ServerUser.db;Version=3;"))
|
||
{
|
||
dbConnection.Open();
|
||
using (var cmd = new SQLiteCommand(@"
|
||
INSERT INTO messages (type, room_id, sender_id, receiver_id, content)
|
||
VALUES (@type, @room_id, @sender_id, @receiver_id, @content)", dbConnection))
|
||
{
|
||
cmd.Parameters.AddWithValue("@type", "group");
|
||
cmd.Parameters.AddWithValue("@room_id", "global");
|
||
cmd.Parameters.AddWithValue("@sender_id", chatData.userid);
|
||
cmd.Parameters.AddWithValue("@receiver_id", DBNull.Value);
|
||
cmd.Parameters.AddWithValue("@content", chatData.message);
|
||
cmd.ExecuteNonQuery();
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
log.Error($"存储消息到数据库失败: {ex.Message}");
|
||
}
|
||
});
|
||
foreach (var user in usersCopy)
|
||
{
|
||
// 查找匹配的客户端
|
||
var targetClient = clientsCopy.FirstOrDefault(c =>
|
||
c.RemoteEndPoint?.ToString() == user.LoginIP);
|
||
|
||
if (targetClient != null)
|
||
{
|
||
try
|
||
{
|
||
// ==== 修改:添加发送异常处理 ====
|
||
SendWithPrefix(targetClient,System.Text.Encoding.UTF8.GetBytes(JsonSerializer.Serialize(chatRegisterData)));
|
||
}
|
||
catch (SocketException ex)
|
||
{
|
||
log.Error($"发送消息到 {user.LoginIP} 失败: {ex.SocketErrorCode}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
log.Error($"发送消息异常: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log.Warn("接收到无效的聊天消息");
|
||
}
|
||
}
|
||
else if (Type.type == "history")
|
||
{
|
||
var historyReq = JsonSerializer.Deserialize<HistoryRequest>(message);
|
||
if (historyReq != null)
|
||
{
|
||
var response = new HistoryResponse();
|
||
string query = "SELECT * FROM messages WHERE ";
|
||
var parameters = new List<SQLiteParameter>();
|
||
|
||
// 构建查询条件
|
||
if (historyReq.chat_type == "group")
|
||
{
|
||
query += "type = 'group' AND room_id = @room_id ";
|
||
parameters.Add(new SQLiteParameter("@room_id", historyReq.room_id ?? "global"));
|
||
}
|
||
// else if (historyReq.chat_type == "private")
|
||
// {
|
||
// query += @"(type = 'private' AND
|
||
//((sender_id = @userid1 AND receiver_id = @userid2) OR
|
||
// (sender_id = @userid2 AND receiver_id = @userid1))) ";
|
||
// parameters.Add(new SQLiteParameter("@userid1", historyReq.sender_id));
|
||
// parameters.Add(new SQLiteParameter("@userid2", historyReq.receiver_id));
|
||
// }
|
||
|
||
// 获取总消息数
|
||
string countQuery = query.Replace("SELECT *", "SELECT COUNT(*)");
|
||
using var countCmd = new SQLiteCommand(countQuery, User_db);
|
||
countCmd.Parameters.AddRange(parameters.ToArray());
|
||
response.total_count = Convert.ToInt32(countCmd.ExecuteScalar());
|
||
|
||
// 获取分页消息
|
||
query += "ORDER BY timestamp DESC LIMIT @count OFFSET @offset";
|
||
using var cmd = new SQLiteCommand(query, User_db);
|
||
cmd.Parameters.AddRange(parameters.ToArray());
|
||
cmd.Parameters.AddWithValue("@count", historyReq.count);
|
||
cmd.Parameters.AddWithValue("@offset", historyReq.offset);
|
||
|
||
using var reader = cmd.ExecuteReader();
|
||
while (reader.Read())
|
||
{
|
||
response.history.Add(new ChatRegisterData
|
||
{
|
||
type = "chat",
|
||
userid = reader["sender_id"].ToString() ?? "Unid",
|
||
user = GetUsernameByUserId(reader["sender_id"].ToString() ?? "Unid"),
|
||
msgtype = MessageType.Text,
|
||
message = reader["content"].ToString(),
|
||
timestamp = Convert.ToDateTime(reader["timestamp"])
|
||
});
|
||
}
|
||
SendWithPrefix(socket,JsonSerializer.SerializeToUtf8Bytes(response));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
log.Warn("未知的请求类型: " + Type.type);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
catch (SocketException ex)
|
||
{
|
||
log.Error("Socket error: " + ex.Message);
|
||
}
|
||
catch (JsonException ex)
|
||
{
|
||
log.Error("JSON parsing error: " + ex.Message);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
log.Error("Error handling client: " + ex.Message);
|
||
}
|
||
finally
|
||
{
|
||
lock (Client_lock)
|
||
{
|
||
log.Info($"用户 {socket.RemoteEndPoint} 已断开连接");
|
||
var disconnectedIp = socket.RemoteEndPoint?.ToString();
|
||
if (!string.IsNullOrEmpty(disconnectedIp))
|
||
{
|
||
LoginUser.RemoveAll(u => u.LoginIP.StartsWith(disconnectedIp));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 压缩数据
|
||
/// </summary>
|
||
/// <param name="data"></param>
|
||
/// <returns></returns>
|
||
private static byte[] Compress(byte[] data)
|
||
{
|
||
if (data.Length < 256)
|
||
return data;
|
||
|
||
using (var compressedStream = new MemoryStream())
|
||
{
|
||
using (var zipStream = new GZipStream(compressedStream, CompressionMode.Compress))
|
||
{
|
||
zipStream.Write(data, 0, data.Length);
|
||
}
|
||
return compressedStream.ToArray();
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 解压缩数据
|
||
/// </summary>
|
||
/// <param name="compressedData"></param>
|
||
/// <returns></returns>
|
||
private static byte[] Decompress(byte[] compressedData)
|
||
{
|
||
if (compressedData.Length < 2 || compressedData[0] != 0x1F || compressedData[1] != 0x8B)
|
||
return compressedData;
|
||
|
||
using (var compressedStream = new MemoryStream(compressedData))
|
||
using (var zipStream = new GZipStream(compressedStream, CompressionMode.Decompress))
|
||
using (var resultStream = new MemoryStream())
|
||
{
|
||
zipStream.CopyTo(resultStream);
|
||
return resultStream.ToArray();
|
||
}
|
||
}
|
||
/// <summary>
|
||
/// 发送数据
|
||
/// </summary>
|
||
/// <param name="socket"></param>
|
||
/// <param name="data"></param>
|
||
private static void SendWithPrefix(Socket socket, byte[] data)
|
||
{
|
||
byte[] compressedData = Compress(data);
|
||
byte[] lengthPrefix = BitConverter.GetBytes(compressedData.Length);
|
||
byte[] fullMessage = new byte[lengthPrefix.Length + compressedData.Length];
|
||
|
||
Buffer.BlockCopy(lengthPrefix, 0, fullMessage, 0, lengthPrefix.Length);
|
||
Buffer.BlockCopy(compressedData, 0, fullMessage, lengthPrefix.Length, compressedData.Length);
|
||
log.Info($"发送数据(长度:{data.Length},压缩后长度:{compressedData.Length},总体长度:{fullMessage.Length})");
|
||
socket.Send(fullMessage);
|
||
}
|
||
/// <summary>
|
||
/// 查询User_db是否有相同用户名
|
||
/// </summary>
|
||
/// <param name="username"></param>
|
||
/// <returns></returns>
|
||
static bool UserExists(string username)
|
||
{
|
||
using var cmd = new SQLiteCommand("SELECT COUNT(*) FROM users WHERE username = @username", User_db);
|
||
cmd.Parameters.AddWithValue("@username", username);
|
||
var count = Convert.ToInt32(cmd.ExecuteScalar());
|
||
return count > 0;
|
||
}
|
||
/// <summary>
|
||
/// 验证用户登录信息并返回userid结果
|
||
/// </summary>
|
||
/// <param name="username"></param>
|
||
/// <param name="password"></param>
|
||
/// <returns></returns>
|
||
static string UserAuthentication(string username, string password)
|
||
{
|
||
using var cmd = new SQLiteCommand("SELECT userid FROM users WHERE username = @username AND password = @password", User_db);
|
||
cmd.Parameters.AddWithValue("@username", username);
|
||
cmd.Parameters.AddWithValue("@password", password);
|
||
var result = cmd.ExecuteScalar();
|
||
return result != null ? result.ToString()! : string.Empty;
|
||
}
|
||
// 在ChatServer类中添加一个方法用于初始化users表
|
||
private static void InitializeTable()
|
||
{
|
||
using var cmd = new SQLiteCommand(@"
|
||
CREATE TABLE IF NOT EXISTS users (
|
||
userid TEXT PRIMARY KEY,
|
||
username TEXT NOT NULL UNIQUE,
|
||
password TEXT NOT NULL
|
||
)", User_db);
|
||
cmd.ExecuteNonQuery();
|
||
// 新增消息表
|
||
using var cmd2 = new SQLiteCommand(@"
|
||
CREATE TABLE IF NOT EXISTS messages (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
type TEXT NOT NULL CHECK(type IN ('group', 'private')),
|
||
room_id TEXT,
|
||
sender_id TEXT NOT NULL REFERENCES users(userid),
|
||
receiver_id TEXT REFERENCES users(userid),
|
||
content TEXT NOT NULL,
|
||
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
)", User_db);
|
||
cmd2.ExecuteNonQuery();
|
||
|
||
// 创建索引提高查询效率
|
||
using var cmd3 = new SQLiteCommand(@"
|
||
CREATE INDEX IF NOT EXISTS idx_messages_timestamp
|
||
ON messages (timestamp DESC)", User_db);
|
||
cmd3.ExecuteNonQuery();
|
||
using var cmd4 = new SQLiteCommand(@"
|
||
CREATE INDEX IF NOT EXISTS idx_messages_room
|
||
ON messages (room_id, timestamp DESC)", User_db);
|
||
cmd4.ExecuteNonQuery();
|
||
}
|
||
/// <summary>
|
||
/// 根据userid查询对应的用户名
|
||
/// </summary>
|
||
/// <param name="userid"></param>
|
||
/// <returns>用户名,如果userid为空或为"Unid",返回默认用户名"Unnamed"</returns>
|
||
static string GetUsernameByUserId(string userid)
|
||
{
|
||
if (string.IsNullOrEmpty(userid) || userid == "Unid")
|
||
{
|
||
return "Unnamed"; // 如果userid为空或为"Unid",返回默认用户名
|
||
}
|
||
using var cmd = new SQLiteCommand("SELECT username FROM users WHERE userid = @userid", User_db);
|
||
cmd.Parameters.AddWithValue("@userid", userid);
|
||
var result = cmd.ExecuteScalar();
|
||
return result != null ? result.ToString()! : "Unnamed";
|
||
}
|
||
|
||
}
|
||
} |