Add 'gtk/' from commit '7d0dfb455aa86245231b383a92e79b3c08a12d5e'
git-subtree-dir: gtk git-subtree-mainline:c710c6e053git-subtree-split:7d0dfb455a
This commit is contained in:
12
gtk/src/service/dbus-service-base.vala
Normal file
12
gtk/src/service/dbus-service-base.vala
Normal file
@@ -0,0 +1,12 @@
|
||||
public abstract class DBusServiceProxy : Object {
|
||||
protected const string DBUS_PATH = "/net/buzzert/kordophonecd/daemon";
|
||||
protected const string DBUS_NAME = "net.buzzert.kordophonecd";
|
||||
|
||||
protected DBusServiceProxy() {
|
||||
}
|
||||
}
|
||||
|
||||
protected errordomain DBusServiceProxyError {
|
||||
NOT_CONNECTED,
|
||||
PASSWORD_STORAGE;
|
||||
}
|
||||
87
gtk/src/service/interface/dbusservice.vala
Normal file
87
gtk/src/service/interface/dbusservice.vala
Normal file
@@ -0,0 +1,87 @@
|
||||
/* Generated by vala-dbus-binding-tool 1.0-aa2fb. Do not modify! */
|
||||
/* Generated with: vala-dbus-binding-tool --no-synced --strip-namespace=net --strip-namespace=buzzert --rename-namespace=kordophone:DBusService --api-path=xml/ */
|
||||
using GLib;
|
||||
|
||||
namespace DBusService {
|
||||
|
||||
[DBus (name = "net.buzzert.kordophone.Settings", timeout = 120000)]
|
||||
public interface Settings : GLib.Object {
|
||||
|
||||
[DBus (name = "ServerURL")]
|
||||
public abstract string server_u_r_l { owned get; set; }
|
||||
|
||||
[DBus (name = "Username")]
|
||||
public abstract string username { owned get; set; }
|
||||
|
||||
[DBus (name = "SetServer")]
|
||||
public abstract void set_server(string url, string user) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "ConfigChanged")]
|
||||
public signal void config_changed();
|
||||
}
|
||||
|
||||
[DBus (name = "net.buzzert.kordophone.Repository", timeout = 120000)]
|
||||
public interface Repository : GLib.Object {
|
||||
|
||||
[DBus (name = "GetVersion")]
|
||||
public abstract string get_version() throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "GetConversations")]
|
||||
public abstract GLib.HashTable<string, GLib.Variant>[] get_conversations(int limit, int offset) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "SyncConversationList")]
|
||||
public abstract void sync_conversation_list() throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "SyncAllConversations")]
|
||||
public abstract void sync_all_conversations() throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "SyncConversation")]
|
||||
public abstract void sync_conversation(string conversation_id) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "MarkConversationAsRead")]
|
||||
public abstract void mark_conversation_as_read(string conversation_id) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "ConversationsUpdated")]
|
||||
public signal void conversations_updated();
|
||||
|
||||
[DBus (name = "DeleteAllConversations")]
|
||||
public abstract void delete_all_conversations() throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "GetMessages")]
|
||||
public abstract GLib.HashTable<string, GLib.Variant>[] get_messages(string conversation_id, string last_message_id) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "SendMessage")]
|
||||
public abstract string send_message(string conversation_id, string text, string[] attachment_guids) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "MessagesUpdated")]
|
||||
public signal void messages_updated(string conversation_id);
|
||||
|
||||
[DBus (name = "UpdateStreamReconnected")]
|
||||
public signal void update_stream_reconnected();
|
||||
|
||||
[DBus (name = "GetAttachmentInfo")]
|
||||
public abstract RepositoryAttachmentInfoStruct get_attachment_info(string attachment_id) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "DownloadAttachment")]
|
||||
public abstract void download_attachment(string attachment_id, bool preview) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "UploadAttachment")]
|
||||
public abstract string upload_attachment(string path) throws DBusError, IOError;
|
||||
|
||||
[DBus (name = "AttachmentDownloadCompleted")]
|
||||
public signal void attachment_download_completed(string attachment_id);
|
||||
|
||||
[DBus (name = "AttachmentDownloadFailed")]
|
||||
public signal void attachment_download_failed(string attachment_id, string error_message);
|
||||
|
||||
[DBus (name = "AttachmentUploadCompleted")]
|
||||
public signal void attachment_upload_completed(string upload_guid, string attachment_guid);
|
||||
}
|
||||
|
||||
public struct RepositoryAttachmentInfoStruct {
|
||||
public string attr1;
|
||||
public string attr2;
|
||||
public bool attr3;
|
||||
public bool attr4;
|
||||
}
|
||||
}
|
||||
10
gtk/src/service/interface/generate.sh
Executable file
10
gtk/src/service/interface/generate.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh
|
||||
|
||||
if ! command -v vala-dbus-binding-tool >/dev/null 2>&1; then
|
||||
echo "Error: vala-dbus-binding-tool not found. Please install it first."
|
||||
echo "https://github.com/freesmartphone/vala-dbus-binding-tool"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
vala-dbus-binding-tool --no-synced --strip-namespace=net --strip-namespace=buzzert --rename-namespace=kordophone:DBusService --api-path=xml/
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<!DOCTYPE node PUBLIC "-//freedesktop//DTD D-BUS Object Introspection 1.0//EN"
|
||||
"http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd">
|
||||
<node>
|
||||
<interface name="net.buzzert.kordophone.Repository">
|
||||
<method name="GetVersion">
|
||||
<arg type="s" name="version" direction="out" />
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Returns the version of the client daemon."/>
|
||||
</method>
|
||||
|
||||
<!-- Conversations -->
|
||||
|
||||
<method name="GetConversations">
|
||||
<arg type="i" name="limit" direction="in"/>
|
||||
<arg type="i" name="offset" direction="in"/>
|
||||
|
||||
<arg type="aa{sv}" direction="out" name="conversations">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Array of dictionaries. Each dictionary has keys:
|
||||
'id' (string): Unique identifier
|
||||
'display_name' (string): Display name
|
||||
'last_message_preview' (string): Preview text
|
||||
'is_unread' (boolean): Unread status
|
||||
'date' (int64): Date of last message
|
||||
'participants' (array of strings): List of participants
|
||||
'unread_count' (int32): Number of unread messages"/>
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<method name="SyncConversationList">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Initiates a background sync of the conversation list with the server."/>
|
||||
</method>
|
||||
|
||||
<method name="SyncAllConversations">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Initiates a background sync of all conversations with the server."/>
|
||||
</method>
|
||||
|
||||
<method name="SyncConversation">
|
||||
<arg type="s" name="conversation_id" direction="in"/>
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Initiates a background sync of a single conversation with the server."/>
|
||||
</method>
|
||||
|
||||
<method name="MarkConversationAsRead">
|
||||
<arg type="s" name="conversation_id" direction="in"/>
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Marks a conversation as read."/>
|
||||
</method>
|
||||
|
||||
<signal name="ConversationsUpdated">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when the list of conversations is updated."/>
|
||||
</signal>
|
||||
|
||||
<method name="DeleteAllConversations">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Deletes all conversations from the database."/>
|
||||
</method>
|
||||
|
||||
<!-- Messages -->
|
||||
|
||||
<method name="GetMessages">
|
||||
<arg type="s" name="conversation_id" direction="in"/>
|
||||
<arg type="s" name="last_message_id" direction="in"/>
|
||||
<arg type="aa{sv}" direction="out" name="messages">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Array of dictionaries. Each dictionary has keys:
|
||||
'id' (string): Unique message identifier
|
||||
'text' (string): Message body text
|
||||
'date' (int64): Message timestamp
|
||||
'sender' (string): Sender display name
|
||||
'attachments' (array of dictionaries): List of attachments
|
||||
'guid' (string): Attachment GUID
|
||||
'path' (string): Attachment path
|
||||
'preview_path' (string): Preview attachment path
|
||||
'downloaded' (boolean): Whether the attachment is downloaded
|
||||
'preview_downloaded' (boolean): Whether the preview is downloaded
|
||||
'metadata' (dictionary, optional): Attachment metadata
|
||||
'attribution_info' (dictionary, optional): Attribution info
|
||||
'width' (int32, optional): Width
|
||||
'height' (int32, optional): Height"/>
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<method name="SendMessage">
|
||||
<arg type="s" name="conversation_id" direction="in"/>
|
||||
<arg type="s" name="text" direction="in"/>
|
||||
<arg type="as" name="attachment_guids" direction="in"/>
|
||||
|
||||
<arg type="s" name="outgoing_message_id" direction="out"/>
|
||||
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Sends a message to the server. Returns the outgoing message ID.
|
||||
Arguments:
|
||||
- conversation_id: The ID of the conversation to send the message to.
|
||||
- text: The text of the message to send.
|
||||
- attachment_guids: The GUIDs of the attachments to send.
|
||||
|
||||
Returns:
|
||||
- outgoing_message_id: The ID of the outgoing message.
|
||||
"/>
|
||||
</method>
|
||||
|
||||
<signal name="MessagesUpdated">
|
||||
<arg type="s" name="conversation_id" direction="in"/>
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when the list of messages is updated."/>
|
||||
</signal>
|
||||
|
||||
<signal name="UpdateStreamReconnected">
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when the update stream is reconnected after a timeout or configuration change."/>
|
||||
</signal>
|
||||
|
||||
<!-- Attachments -->
|
||||
|
||||
<method name="GetAttachmentInfo">
|
||||
<arg type="s" name="attachment_id" direction="in"/>
|
||||
<arg type="(ssbb)" name="attachment_info" direction="out"/>
|
||||
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Returns attachment info:
|
||||
- path: string
|
||||
- preview_path: string
|
||||
- downloaded: boolean
|
||||
- preview_downloaded: boolean
|
||||
"/>
|
||||
</method>
|
||||
|
||||
<method name="DownloadAttachment">
|
||||
<arg type="s" name="attachment_id" direction="in"/>
|
||||
<arg type="b" name="preview" direction="in"/>
|
||||
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Initiates download of the specified attachment if not already downloaded.
|
||||
Arguments:
|
||||
attachment_id: the attachment GUID
|
||||
preview: whether to download the preview (true) or full attachment (false)
|
||||
"/>
|
||||
</method>
|
||||
|
||||
<method name="UploadAttachment">
|
||||
<arg type="s" name="path" direction="in"/>
|
||||
<arg type="s" name="upload_guid" direction="out"/>
|
||||
</method>
|
||||
|
||||
<signal name="AttachmentDownloadCompleted">
|
||||
<arg type="s" name="attachment_id"/>
|
||||
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when an attachment download completes successfully."/>
|
||||
</signal>
|
||||
|
||||
<signal name="AttachmentDownloadFailed">
|
||||
<arg type="s" name="attachment_id"/>
|
||||
<arg type="s" name="error_message"/>
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when an attachment download fails."/>
|
||||
</signal>
|
||||
|
||||
<signal name="AttachmentUploadCompleted">
|
||||
<arg type="s" name="upload_guid"/>
|
||||
<arg type="s" name="attachment_guid"/>
|
||||
|
||||
<annotation name="org.freedesktop.DBus.DocString"
|
||||
value="Emitted when an attachment upload completes successfully.
|
||||
Returns:
|
||||
- upload_guid: The GUID of the upload.
|
||||
- attachment_guid: The GUID of the attachment on the server.
|
||||
"/>
|
||||
</signal>
|
||||
</interface>
|
||||
|
||||
<interface name="net.buzzert.kordophone.Settings">
|
||||
<!-- editable properties -->
|
||||
<property name="ServerURL" type="s" access="readwrite"/>
|
||||
<property name="Username" type="s" access="readwrite"/>
|
||||
|
||||
<!-- helpers for atomic updates -->
|
||||
<method name="SetServer">
|
||||
<arg name="url" type="s" direction="in"/>
|
||||
<arg name="user" type="s" direction="in"/>
|
||||
</method>
|
||||
|
||||
<!-- emitted when anything changes -->
|
||||
<signal name="ConfigChanged"/>
|
||||
</interface>
|
||||
</node>
|
||||
147
gtk/src/service/repository.vala
Normal file
147
gtk/src/service/repository.vala
Normal file
@@ -0,0 +1,147 @@
|
||||
using GLib;
|
||||
using Gee;
|
||||
|
||||
public class Repository : DBusServiceProxy {
|
||||
public signal void conversations_updated();
|
||||
public signal void messages_updated(string conversation_guid);
|
||||
public signal void attachment_downloaded(string attachment_guid);
|
||||
public signal void attachment_uploaded(string upload_guid, string attachment_guid);
|
||||
public signal void reconnected();
|
||||
|
||||
public static Repository get_instance() {
|
||||
if (instance == null) {
|
||||
instance = new Repository();
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
private static Repository instance = null;
|
||||
private DBusService.Repository? dbus_repository;
|
||||
private uint dbus_watch_id;
|
||||
|
||||
private Repository() {
|
||||
this.dbus_watch_id = Bus.watch_name(BusType.SESSION, DBUS_NAME, BusNameWatcherFlags.AUTO_START, (name, name_owner) => {
|
||||
connect_to_repository();
|
||||
});
|
||||
}
|
||||
|
||||
private void connect_to_repository() {
|
||||
GLib.info("Connecting to repository");
|
||||
|
||||
try {
|
||||
this.dbus_repository = Bus.get_proxy_sync<DBusService.Repository>(BusType.SESSION, DBUS_NAME, DBUS_PATH);
|
||||
this.dbus_repository.conversations_updated.connect(() => {
|
||||
conversations_updated();
|
||||
});
|
||||
|
||||
this.dbus_repository.messages_updated.connect((conversation_guid) => {
|
||||
messages_updated(conversation_guid);
|
||||
});
|
||||
|
||||
this.dbus_repository.attachment_download_completed.connect((attachment_guid) => {
|
||||
attachment_downloaded(attachment_guid);
|
||||
});
|
||||
|
||||
this.dbus_repository.attachment_upload_completed.connect((upload_guid, attachment_guid) => {
|
||||
attachment_uploaded(upload_guid, attachment_guid);
|
||||
});
|
||||
|
||||
this.dbus_repository.update_stream_reconnected.connect(() => {
|
||||
reconnected();
|
||||
});
|
||||
|
||||
conversations_updated();
|
||||
reconnected();
|
||||
} catch (GLib.Error e) {
|
||||
warning("Failed to connect to repository: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public void sync_conversation_list() throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
dbus_repository.sync_conversation_list();
|
||||
}
|
||||
|
||||
public Conversation[] get_conversations(int limit = 200) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
var conversations = dbus_repository.get_conversations(limit, 0);
|
||||
Conversation[] returned_conversations = new Conversation[conversations.length];
|
||||
|
||||
for (int i = 0; i < conversations.length; i++) {
|
||||
returned_conversations[i] = new Conversation.from_hash_table(conversations[i]);
|
||||
}
|
||||
|
||||
return returned_conversations;
|
||||
}
|
||||
|
||||
public Message[] get_messages(string conversation_guid, string last_message_id = "") throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
var messages = dbus_repository.get_messages(conversation_guid, last_message_id);
|
||||
Message[] returned_messages = new Message[messages.length];
|
||||
|
||||
for (int i = 0; i < messages.length; i++) {
|
||||
returned_messages[i] = new Message.from_hash_table(messages[i]);
|
||||
}
|
||||
|
||||
return returned_messages;
|
||||
}
|
||||
|
||||
public string send_message(string conversation_guid, string message, string[] attachment_guids) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
return dbus_repository.send_message(conversation_guid, message, attachment_guids);
|
||||
}
|
||||
|
||||
public void sync_conversation(string conversation_guid) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
dbus_repository.sync_conversation(conversation_guid);
|
||||
}
|
||||
|
||||
public void mark_conversation_as_read(string conversation_guid) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
dbus_repository.mark_conversation_as_read(conversation_guid);
|
||||
}
|
||||
|
||||
public void download_attachment(string attachment_guid, bool preview) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
dbus_repository.download_attachment(attachment_guid, preview);
|
||||
}
|
||||
|
||||
public string upload_attachment(string filename) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
return dbus_repository.upload_attachment(filename);
|
||||
}
|
||||
|
||||
public AttachmentInfo get_attachment_info(string attachment_guid) throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_repository == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Repository not connected");
|
||||
}
|
||||
|
||||
var info = dbus_repository.get_attachment_info(attachment_guid);
|
||||
return new AttachmentInfo(info.attr1, info.attr2, info.attr3, info.attr4);
|
||||
}
|
||||
}
|
||||
94
gtk/src/service/settings.vala
Normal file
94
gtk/src/service/settings.vala
Normal file
@@ -0,0 +1,94 @@
|
||||
using GLib;
|
||||
|
||||
public class Settings : DBusServiceProxy
|
||||
{
|
||||
public signal void config_changed();
|
||||
public signal void settings_ready();
|
||||
|
||||
private DBusService.Settings? dbus_settings;
|
||||
private Secret.Service secret_service;
|
||||
|
||||
public Settings() {
|
||||
base();
|
||||
|
||||
try {
|
||||
secret_service = Secret.Service.get_sync(Secret.ServiceFlags.OPEN_SESSION);
|
||||
|
||||
this.dbus_settings = Bus.get_proxy_sync<DBusService.Settings>(BusType.SESSION, DBUS_NAME, DBUS_PATH);
|
||||
settings_ready();
|
||||
} catch (GLib.Error e) {
|
||||
warning("Failed to get secret service: %s", e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public string get_server_url() throws DBusServiceProxyError, GLib.Error {
|
||||
if (dbus_settings == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected");
|
||||
}
|
||||
return dbus_settings.server_u_r_l;
|
||||
}
|
||||
|
||||
public void set_server_url(string url) throws Error, GLib.Error {
|
||||
if (dbus_settings == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected");
|
||||
}
|
||||
dbus_settings.server_u_r_l = url;
|
||||
}
|
||||
|
||||
public string get_username() throws Error, GLib.Error {
|
||||
if (dbus_settings == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected");
|
||||
}
|
||||
return dbus_settings.username;
|
||||
}
|
||||
|
||||
public void set_username(string username) throws Error, GLib.Error {
|
||||
if (dbus_settings == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected");
|
||||
}
|
||||
dbus_settings.username = username;
|
||||
}
|
||||
|
||||
public void set_server(string url, string username) throws Error, GLib.Error {
|
||||
if (dbus_settings == null) {
|
||||
throw new DBusServiceProxyError.NOT_CONNECTED("Settings not connected");
|
||||
}
|
||||
dbus_settings.set_server(url, username);
|
||||
}
|
||||
|
||||
private HashTable<string, string> password_attributes() {
|
||||
var attributes = new HashTable<string, string>(str_hash, str_equal);
|
||||
attributes["service"] = "net.buzzert.kordophonecd";
|
||||
attributes["target"] = "default";
|
||||
attributes["username"] = get_username();
|
||||
return attributes;
|
||||
}
|
||||
|
||||
public string get_password() throws Error {
|
||||
var attributes = password_attributes();
|
||||
var password = secret_service.lookup_sync(null, attributes, null);
|
||||
if (password == null) {
|
||||
warning("No password found for user %s", get_username());
|
||||
return "";
|
||||
}
|
||||
|
||||
return password.get_text();
|
||||
}
|
||||
|
||||
public void set_password(string password) throws Error, GLib.Error {
|
||||
var attributes = password_attributes();
|
||||
bool result = secret_service.store_sync(
|
||||
null,
|
||||
attributes,
|
||||
Secret.COLLECTION_DEFAULT,
|
||||
"Kordophone Keystore",
|
||||
new Secret.Value(password, password.length, "text/plain"),
|
||||
null
|
||||
);
|
||||
|
||||
if (!result) {
|
||||
warning("Failed to store password for user %s", get_username());
|
||||
throw new DBusServiceProxyError.PASSWORD_STORAGE("Failed to store password");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user