database is now a class, and the file is in ~/.config.
This commit is contained in:
@@ -2,185 +2,190 @@ import sqlite3
|
|||||||
from common import gajim
|
from common import gajim
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
path = sys.path[1]
|
|
||||||
path = path + '/file_sharing/' + 'shared_files.db'
|
|
||||||
db_exist = os.path.exists(path)
|
|
||||||
conn = sqlite3.connect(path)
|
|
||||||
# Enable foreign keys contraints
|
|
||||||
conn.cursor().execute("pragma foreign_keys = on")
|
|
||||||
|
|
||||||
# NOTE: Make sure we are getting and setting the requester without its resource
|
class FilesharingDatabase:
|
||||||
def create_database():
|
def __init__(self, plugin):
|
||||||
c = conn.cursor()
|
self.plugin = plugin
|
||||||
# Create tables
|
path_l = os.path.split(plugin.config.FILE_PATH)
|
||||||
c.execute("CREATE TABLE permissions" +
|
path = os.path.join(path_l[0], 'shared_files.db')
|
||||||
"(fid integer REFERENCES files(fid) ON DELETE CASCADE, " +
|
db_exist = os.path.exists(path)
|
||||||
"account text, requester text)")
|
print path
|
||||||
c.execute("CREATE TABLE files" +
|
self.conn = sqlite3.connect(path)
|
||||||
"(fid INTEGER PRIMARY KEY AUTOINCREMENT," +
|
# Enable foreign keys contraints
|
||||||
" file_path text, relative_path text, hash_sha1 text," +
|
self.conn.cursor().execute("pragma foreign_keys = on")
|
||||||
"size numeric, description text, mod_date text, is_dir boolean)")
|
if not db_exist:
|
||||||
# Save (commit) the changes
|
self.create_database()
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
|
|
||||||
def get_toplevel_files(account, requester):
|
# NOTE: Make sure we are getting and setting the requester without its resource
|
||||||
c = conn.cursor()
|
def create_database(self):
|
||||||
data = (account, requester)
|
c = self.conn.cursor()
|
||||||
c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," +
|
# Create tables
|
||||||
" is_dir FROM (files JOIN permissions ON" +
|
c.execute("CREATE TABLE permissions" +
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
"(fid integer REFERENCES files(fid) ON DELETE CASCADE, " +
|
||||||
" AND relative_path NOT LIKE '%/%'", data)
|
"account text, requester text)")
|
||||||
result = c.fetchall()
|
c.execute("CREATE TABLE files" +
|
||||||
c.close()
|
"(fid INTEGER PRIMARY KEY AUTOINCREMENT," +
|
||||||
return result
|
" file_path text, relative_path text, hash_sha1 text," +
|
||||||
|
"size numeric, description text, mod_date text, is_dir boolean)")
|
||||||
|
# Save (commit) the changes
|
||||||
|
self.conn.commit()
|
||||||
|
c.close()
|
||||||
|
|
||||||
def get_files_from_dir(account, requester, dir_):
|
def get_toplevel_files(self, account, requester):
|
||||||
c = conn.cursor()
|
c = self.conn.cursor()
|
||||||
data = (account, requester, dir_ + '/%')
|
data = (account, requester)
|
||||||
c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," +
|
c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," +
|
||||||
" is_dir FROM (files JOIN permissions ON" +
|
" is_dir FROM (files JOIN permissions ON" +
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
||||||
" AND relative_path LIKE ?", data)
|
" AND relative_path NOT LIKE '%/%'", data)
|
||||||
result = c.fetchall()
|
result = c.fetchall()
|
||||||
c.close()
|
c.close()
|
||||||
fresult = []
|
return result
|
||||||
for r in result:
|
|
||||||
name = r[0][len(dir_) + 1:]
|
|
||||||
if '/' not in name:
|
|
||||||
fresult.append(r)
|
|
||||||
return fresult
|
|
||||||
|
|
||||||
def get_files(account, requester):
|
def get_files_from_dir(self, account, requester, dir_):
|
||||||
"""
|
c = self.conn.cursor()
|
||||||
>>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \
|
data = (account, requester, dir_ + '/%')
|
||||||
'date', False)
|
c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," +
|
||||||
>>> foo = add_file('account@gajim', 'requester@jabber', file_)
|
" is_dir FROM (files JOIN permissions ON" +
|
||||||
>>> result = get_files('account@gajim', 'requester@jabber')
|
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
||||||
>>> len(result)
|
" AND relative_path LIKE ?", data)
|
||||||
1
|
result = c.fetchall()
|
||||||
>>> _delete_file(1)
|
c.close()
|
||||||
"""
|
fresult = []
|
||||||
c = conn.cursor()
|
for r in result:
|
||||||
data = (account, requester)
|
name = r[0][len(dir_) + 1:]
|
||||||
c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," +
|
if '/' not in name:
|
||||||
" is_dir FROM (files JOIN permissions ON" +
|
fresult.append(r)
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?", data)
|
return fresult
|
||||||
result = c.fetchall()
|
|
||||||
c.close()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def get_file(account, requester, hash_, name):
|
def get_files(self, account, requester):
|
||||||
c = conn.cursor()
|
"""
|
||||||
if hash_:
|
>>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \
|
||||||
data = (account, requester, hash_)
|
'date', False)
|
||||||
sql = "SELECT relative_path, hash_sha1, size, description, mod_date," + \
|
>>> foo = add_file('account@gajim', 'requester@jabber', file_)
|
||||||
" file_path FROM (files JOIN permissions ON" + \
|
>>> result = get_files('account@gajim', 'requester@jabber')
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" + \
|
>>> len(result)
|
||||||
" AND hash_sha1=?"
|
1
|
||||||
else:
|
>>> _delete_file(1)
|
||||||
data = (account, requester, name)
|
"""
|
||||||
sql = "SELECT relative_path, hash_sha1, size, description, mod_date," + \
|
c = self.conn.cursor()
|
||||||
" file_path FROM (files JOIN permissions ON" + \
|
data = (account, requester)
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" + \
|
c.execute("SELECT relative_path, hash_sha1, size, description, mod_date," +
|
||||||
" AND relative_path=?"
|
" is_dir FROM (files JOIN permissions ON" +
|
||||||
c.execute(sql, data)
|
" files.fid=permissions.fid) WHERE account=? AND requester=?", data)
|
||||||
result = c.fetchall()
|
result = c.fetchall()
|
||||||
c.close()
|
c.close()
|
||||||
if result == []:
|
return result
|
||||||
return None
|
|
||||||
else:
|
|
||||||
return result[0]
|
|
||||||
|
|
||||||
def get_files_name(account, requester):
|
def get_file(self, account, requester, hash_, name):
|
||||||
result = get_files(account, requester)
|
c = self.conn.cursor()
|
||||||
flist = []
|
if hash_:
|
||||||
for r in result:
|
data = (account, requester, hash_)
|
||||||
flist.append(r[0])
|
sql = "SELECT relative_path, hash_sha1, size, description, mod_date," + \
|
||||||
return flist
|
" file_path FROM (files JOIN permissions ON" + \
|
||||||
|
" files.fid=permissions.fid) WHERE account=? AND requester=?" + \
|
||||||
|
" AND hash_sha1=?"
|
||||||
|
else:
|
||||||
|
data = (account, requester, name)
|
||||||
|
sql = "SELECT relative_path, hash_sha1, size, description, mod_date," + \
|
||||||
|
" file_path FROM (files JOIN permissions ON" + \
|
||||||
|
" files.fid=permissions.fid) WHERE account=? AND requester=?" + \
|
||||||
|
" AND relative_path=?"
|
||||||
|
c.execute(sql, data)
|
||||||
|
result = c.fetchall()
|
||||||
|
c.close()
|
||||||
|
if result == []:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
return result[0]
|
||||||
|
|
||||||
def add_file(account, requester, file_):
|
def get_files_name(self, account, requester):
|
||||||
"""
|
result = self.get_files(account, requester)
|
||||||
>>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \
|
flist = []
|
||||||
'date', False)
|
for r in result:
|
||||||
>>> add_file('account@gajim', 'requester@jabber', file_)
|
flist.append(r[0])
|
||||||
1
|
return flist
|
||||||
>>> _delete_file(1)
|
|
||||||
"""
|
|
||||||
_check_duplicate(account, requester, file_)
|
|
||||||
requester = gajim.get_jid_without_resource(requester)
|
|
||||||
c = conn.cursor()
|
|
||||||
c.execute("INSERT INTO files (file_path, " +
|
|
||||||
"relative_path, hash_sha1, size, description, mod_date, " +
|
|
||||||
" is_dir) VALUES (?,?,?,?,?,?,?)",
|
|
||||||
file_)
|
|
||||||
fid = c.lastrowid
|
|
||||||
permission_data = (fid, account, requester)
|
|
||||||
c.execute("INSERT INTO permissions VALUES (?,?,?)", permission_data)
|
|
||||||
conn.commit()
|
|
||||||
c.close()
|
|
||||||
return fid
|
|
||||||
|
|
||||||
def _check_duplicate(account, requester, file_):
|
def add_file(self, account, requester, file_):
|
||||||
c = conn.cursor()
|
"""
|
||||||
data = (account, requester, file_[1])
|
>>> file_ = ('file_path', 'relative_path', 'hash', 999, 'description', \
|
||||||
c.execute("SELECT * FROM (files JOIN permissions ON" +
|
'date', False)
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
>>> add_file('account@gajim', 'requester@jabber', file_)
|
||||||
" AND relative_path=? ", data)
|
1
|
||||||
result = c.fetchall()
|
>>> _delete_file(1)
|
||||||
if file_[2] != '':
|
"""
|
||||||
data = (account, requester, file_[2])
|
self._check_duplicate(account, requester, file_)
|
||||||
|
requester = gajim.get_jid_without_resource(requester)
|
||||||
|
c = self.conn.cursor()
|
||||||
|
c.execute("INSERT INTO files (file_path, " +
|
||||||
|
"relative_path, hash_sha1, size, description, mod_date, " +
|
||||||
|
" is_dir) VALUES (?,?,?,?,?,?,?)",
|
||||||
|
file_)
|
||||||
|
fid = c.lastrowid
|
||||||
|
permission_data = (fid, account, requester)
|
||||||
|
c.execute("INSERT INTO permissions VALUES (?,?,?)", permission_data)
|
||||||
|
self.conn.commit()
|
||||||
|
c.close()
|
||||||
|
return fid
|
||||||
|
|
||||||
|
def _check_duplicate(self, account, requester, file_):
|
||||||
|
c = self.conn.cursor()
|
||||||
|
data = (account, requester, file_[1])
|
||||||
c.execute("SELECT * FROM (files JOIN permissions ON" +
|
c.execute("SELECT * FROM (files JOIN permissions ON" +
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
||||||
" AND hash_sha1=?)", data)
|
" AND relative_path=? ", data)
|
||||||
result.extend(c.fetchall())
|
result = c.fetchall()
|
||||||
if len(result) > 0:
|
if file_[2] != '':
|
||||||
raise Exception('Duplicated entry')
|
data = (account, requester, file_[2])
|
||||||
c.close()
|
c.execute("SELECT * FROM (files JOIN permissions ON" +
|
||||||
|
" files.fid=permissions.fid) WHERE account=? AND requester=?" +
|
||||||
|
" AND hash_sha1=?)", data)
|
||||||
|
result.extend(c.fetchall())
|
||||||
|
if len(result) > 0:
|
||||||
|
raise Exception('Duplicated entry')
|
||||||
|
c.close()
|
||||||
|
|
||||||
def _delete_file(fid):
|
def _delete_file(self, fid):
|
||||||
c = conn.cursor()
|
c = self.conn.cursor()
|
||||||
data = (fid, )
|
data = (fid, )
|
||||||
c.execute("DELETE FROM files WHERE fid=?", data)
|
c.execute("DELETE FROM files WHERE fid=?", data)
|
||||||
conn.commit()
|
self.conn.commit()
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
def _delete_dir(dir_, account, requester):
|
def _delete_dir(self, dir_, account, requester):
|
||||||
c = conn.cursor()
|
c = self.conn.cursor()
|
||||||
data = (account, requester, dir_, dir_ + '/%')
|
data = (account, requester, dir_, dir_ + '/%')
|
||||||
sql = "DELETE FROM files WHERE fid IN " + \
|
sql = "DELETE FROM files WHERE fid IN " + \
|
||||||
" (SELECT files.fid FROM files, permissions WHERE" + \
|
" (SELECT files.fid FROM files, permissions WHERE" + \
|
||||||
" files.fid=permissions.fid AND account=?"+ \
|
" files.fid=permissions.fid AND account=?"+ \
|
||||||
" AND requester=? AND (relative_path=? OR relative_path LIKE ?))"
|
" AND requester=? AND (relative_path=? OR relative_path LIKE ?))"
|
||||||
c.execute(sql, data)
|
c.execute(sql, data)
|
||||||
conn.commit()
|
self.conn.commit()
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
def delete(account, requester, relative_path):
|
def delete(self, account, requester, relative_path):
|
||||||
c = conn.cursor()
|
c = self.conn.cursor()
|
||||||
data = (account, requester, relative_path)
|
data = (account, requester, relative_path)
|
||||||
c.execute("SELECT files.fid, is_dir FROM (files JOIN permissions ON" +
|
c.execute("SELECT files.fid, is_dir FROM (files JOIN permissions ON" +
|
||||||
" files.fid=permissions.fid) WHERE account=? AND requester=? AND " +
|
" files.fid=permissions.fid) WHERE account=? AND requester=? AND " +
|
||||||
"relative_path=? ", data)
|
"relative_path=? ", data)
|
||||||
result = c.fetchone()
|
result = c.fetchone()
|
||||||
c.close()
|
c.close()
|
||||||
if result[1] == 0:
|
if result[1] == 0:
|
||||||
_delete_file(result[0])
|
self._delete_file(result[0])
|
||||||
else:
|
else:
|
||||||
_delete_dir(relative_path, account, requester)
|
self._delete_dir(relative_path, account, requester)
|
||||||
|
|
||||||
def delete_all(account, requester):
|
def delete_all(self, account, requester):
|
||||||
c = conn.cursor()
|
c = self.conn.cursor()
|
||||||
data = (account, requester)
|
data = (account, requester)
|
||||||
sql = "DELETE FROM files WHERE fid IN (SELECT fid FROM permissions" + \
|
sql = "DELETE FROM files WHERE fid IN (SELECT fid FROM permissions" + \
|
||||||
" WHERE account=? AND requester=?)"
|
" WHERE account=? AND requester=?)"
|
||||||
c.execute(sql, data)
|
c.execute(sql, data)
|
||||||
conn.commit()
|
self.conn.commit()
|
||||||
c.close()
|
c.close()
|
||||||
|
|
||||||
|
|
||||||
if not db_exist:
|
|
||||||
create_database()
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
"""
|
"""
|
||||||
DELETE DATABASE FILE BEFORE RUNNING TESTS
|
DELETE DATABASE FILE BEFORE RUNNING TESTS
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import database
|
|
||||||
import gtk
|
import gtk
|
||||||
import gobject
|
import gobject
|
||||||
from common import gajim
|
from common import gajim
|
||||||
@@ -219,7 +218,7 @@ class FileShareWindow(gtk.Window):
|
|||||||
file_ = (f, relative_name, '', size, '', mod_date, is_dir)
|
file_ = (f, relative_name, '', size, '', mod_date, is_dir)
|
||||||
requester = self.cbb_contacts.get_active_text()
|
requester = self.cbb_contacts.get_active_text()
|
||||||
try:
|
try:
|
||||||
fid = database.add_file(self.account, requester, file_)
|
fid = self.plugin.database.add_file(self.account, requester, file_)
|
||||||
except Exception, e:
|
except Exception, e:
|
||||||
if e == 'Duplicated entry':
|
if e == 'Duplicated entry':
|
||||||
print 'Error: ' + e
|
print 'Error: ' + e
|
||||||
@@ -263,7 +262,7 @@ class FileShareWindow(gtk.Window):
|
|||||||
self.contacts_rows.append(r)
|
self.contacts_rows.append(r)
|
||||||
self.manage_vbox2.set_sensitive(True)
|
self.manage_vbox2.set_sensitive(True)
|
||||||
self.bt_remove.set_sensitive(False)
|
self.bt_remove.set_sensitive(False)
|
||||||
self.add_file_list(database.get_files_name(self.account,
|
self.add_file_list(self.plugin.database.get_files_name(self.account,
|
||||||
gajim.get_jid_without_resource(contact.get_full_jid())),
|
gajim.get_jid_without_resource(contact.get_full_jid())),
|
||||||
self.ts_files
|
self.ts_files
|
||||||
)
|
)
|
||||||
@@ -281,7 +280,7 @@ class FileShareWindow(gtk.Window):
|
|||||||
# If the contact in the comboboxentry is include inside of the
|
# If the contact in the comboboxentry is include inside of the
|
||||||
# combobox
|
# combobox
|
||||||
if contact == self.ts_contacts.get_value(i, 0):
|
if contact == self.ts_contacts.get_value(i, 0):
|
||||||
self.add_file_list(database.get_files_name(self.account,
|
self.add_file_list(self.plugin.database.get_files_name(self.account,
|
||||||
contact),
|
contact),
|
||||||
self.ts_files
|
self.ts_files
|
||||||
)
|
)
|
||||||
@@ -295,13 +294,13 @@ class FileShareWindow(gtk.Window):
|
|||||||
sel = self.treeSelection_files.get_selected()
|
sel = self.treeSelection_files.get_selected()
|
||||||
relative_name = self.ts_files.get_value(sel[1], 0)
|
relative_name = self.ts_files.get_value(sel[1], 0)
|
||||||
self.ts_files.remove(sel[1])
|
self.ts_files.remove(sel[1])
|
||||||
database.delete(self.account, contact, relative_name)
|
self.plugin.database.delete(self.account, contact, relative_name)
|
||||||
widget.set_sensitive(False)
|
widget.set_sensitive(False)
|
||||||
|
|
||||||
def remove_all_clicked(self, widget, data=None):
|
def remove_all_clicked(self, widget, data=None):
|
||||||
entry = self.cbb_contacts.child
|
entry = self.cbb_contacts.child
|
||||||
contact = entry.get_text()
|
contact = entry.get_text()
|
||||||
database.delete_all(self.account, contact)
|
self.plugin.database.delete_all(self.account, contact)
|
||||||
self.ts_files.clear()
|
self.ts_files.clear()
|
||||||
|
|
||||||
def row_selected(self, widget, data=None):
|
def row_selected(self, widget, data=None):
|
||||||
|
|||||||
@@ -30,11 +30,12 @@ class FileSharePlugin(GajimPlugin):
|
|||||||
self.config_dialog = None
|
self.config_dialog = None
|
||||||
home_path = os.path.expanduser('~/')
|
home_path = os.path.expanduser('~/')
|
||||||
self.config_default_values = {'incoming_dir': (home_path, '')}
|
self.config_default_values = {'incoming_dir': (home_path, '')}
|
||||||
|
self.database = database.FilesharingDatabase(self)
|
||||||
# Create one protocol handler per account
|
# Create one protocol handler per account
|
||||||
accounts = gajim.contacts.get_accounts()
|
accounts = gajim.contacts.get_accounts()
|
||||||
for account in gajim.contacts.get_accounts():
|
for account in gajim.contacts.get_accounts():
|
||||||
FileSharePlugin.prohandler[account] = \
|
FileSharePlugin.prohandler[account] = \
|
||||||
fshare_protocol.protocol(account)
|
fshare_protocol.Protocol(account, self)
|
||||||
self.events_handlers = {
|
self.events_handlers = {
|
||||||
'raw-iq-received': (ged.CORE, self._nec_raw_iq)
|
'raw-iq-received': (ged.CORE, self._nec_raw_iq)
|
||||||
}
|
}
|
||||||
@@ -122,7 +123,7 @@ class FileSharePlugin(GajimPlugin):
|
|||||||
file_info = self._get_file_info(hash_, name)
|
file_info = self._get_file_info(hash_, name)
|
||||||
if file_info:
|
if file_info:
|
||||||
return file_info
|
return file_info
|
||||||
raw_info = database.get_file(account, peerjid, hash_, name)
|
raw_info = self.database.get_file(account, peerjid, hash_, name)
|
||||||
file_info = {'name': raw_info[0],
|
file_info = {'name': raw_info[0],
|
||||||
'file-name' : raw_info[5],
|
'file-name' : raw_info[5],
|
||||||
'hash' : raw_info[1],
|
'hash' : raw_info[1],
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ from common import helpers
|
|||||||
from common import gajim
|
from common import gajim
|
||||||
from common import XMPPDispatcher
|
from common import XMPPDispatcher
|
||||||
from common.xmpp import Hashes
|
from common.xmpp import Hashes
|
||||||
import database
|
|
||||||
# Namespace for file sharing
|
# Namespace for file sharing
|
||||||
NS_FILE_SHARING = 'http://gajim.org/protocol/filesharing'
|
NS_FILE_SHARING = 'http://gajim.org/protocol/filesharing'
|
||||||
|
|
||||||
class protocol():
|
class Protocol():
|
||||||
|
|
||||||
def __init__(self, account):
|
def __init__(self, account, plugin):
|
||||||
self.account = account
|
self.account = account
|
||||||
|
self.plugin = plugin
|
||||||
self.conn = gajim.connections[self.account]
|
self.conn = gajim.connections[self.account]
|
||||||
# get our jid with resource
|
# get our jid with resource
|
||||||
self.ourjid = gajim.get_jid_from_account(self.account)
|
self.ourjid = gajim.get_jid_from_account(self.account)
|
||||||
@@ -52,12 +52,12 @@ class protocol():
|
|||||||
if req.getTag('directory') and not \
|
if req.getTag('directory') and not \
|
||||||
req.getTag('directory').getChildren():
|
req.getTag('directory').getChildren():
|
||||||
# We just received a toplevel directory request
|
# We just received a toplevel directory request
|
||||||
files = database.get_toplevel_files(self.account, jid)
|
files = self.plugin.database.get_toplevel_files(self.account, jid)
|
||||||
response = self.offer(stanza.getID(), fjid, files)
|
response = self.offer(stanza.getID(), fjid, files)
|
||||||
self.conn.connection.send(response)
|
self.conn.connection.send(response)
|
||||||
elif req.getTag('directory') and req.getTag('directory').getTag('name'):
|
elif req.getTag('directory') and req.getTag('directory').getTag('name'):
|
||||||
dir_ = req.getTag('directory').getTag('name').getData()[1:]
|
dir_ = req.getTag('directory').getTag('name').getData()[1:]
|
||||||
files = database.get_files_from_dir(self.account, jid, dir_)
|
files = self.plugin.database.get_files_from_dir(self.account, jid, dir_)
|
||||||
response = self.offer(stanza.getID(), fjid, files)
|
response = self.offer(stanza.getID(), fjid, files)
|
||||||
self.conn.connection.send(response)
|
self.conn.connection.send(response)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user