├── .gitignore ├── README.md ├── res ├── faceboxx_logo.gif ├── faceboxx_logo.png └── sad_face.png ├── screenshots ├── Application.PNG ├── Splash Screen.PNG ├── facebook_1.PNG ├── facebook_2.PNG ├── login name.PNG └── password.PNG └── src ├── chunk.py ├── download.py ├── encryption.py ├── fbIO.py ├── fbupload.py └── gui.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | 56 | messages.txt 57 | screenshot.jpg 58 | *.swp 59 | links.txt 60 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # faceboxx 2 | 3 | http://devpost.com/software/faceboxx 4 | 5 | Faceboxx is an open-source desktop front-end that uses Facebook Messenger as a cloud storage service. It is currently set to break the file into 10MB chunks as per the constant specified in chunk.py. 6 | 7 | To run, execute src/gui.py and enter a username and password when prompted. Then select the "browse" button to select a file on your computer that is greater than 10MB large. Make sure that Firefox is installed in the default location on your system and that you have a working Internet connection. 8 | 9 | Currently, the encrypted zip function uses the pyminizip library, which only works on Linux (https://github.com/smihica/pyminizip/issues/3). A version that works with the 7zip commandline interface is in the works. 10 | 11 | The script also requires the Selenium WebDriver Python library (http://docs.seleniumhq.org/projects/webdriver/). 12 | -------------------------------------------------------------------------------- /res/faceboxx_logo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/res/faceboxx_logo.gif -------------------------------------------------------------------------------- /res/faceboxx_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/res/faceboxx_logo.png -------------------------------------------------------------------------------- /res/sad_face.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/res/sad_face.png -------------------------------------------------------------------------------- /screenshots/Application.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/screenshots/Application.PNG -------------------------------------------------------------------------------- /screenshots/Splash Screen.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/screenshots/Splash Screen.PNG -------------------------------------------------------------------------------- /screenshots/facebook_1.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/screenshots/facebook_1.PNG -------------------------------------------------------------------------------- /screenshots/facebook_2.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/screenshots/facebook_2.PNG -------------------------------------------------------------------------------- /screenshots/login name.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/screenshots/login name.PNG -------------------------------------------------------------------------------- /screenshots/password.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/liamkirsh/faceboxx/0b4a668898b0eedfd52ed4ac91f54a7d68ff90f6/screenshots/password.PNG -------------------------------------------------------------------------------- /src/chunk.py: -------------------------------------------------------------------------------- 1 | #break a file into chunks and join them back 2 | 3 | import pyminizip 4 | import sys 5 | from zipfile import ZipFile 6 | import shutil 7 | import os 8 | 9 | # The compression level passed to pyminizip 10 | compression_level = 9 11 | 12 | # Number of bytes per chunk uploaded to Facebook 13 | CHUNK_SIZE = 10000000 14 | 15 | # Zips inputFile under password and outputs it to output using pyminizip 16 | def zipcrypt(inputFile, output, password): 17 | pyminizip.compress(inputFile, output, password, compression_level) 18 | return 19 | 20 | # Uses built-in zipfile library to decrypt the zip and extract the contents to the current dir 21 | def zipdecrypt(inputFile, password): 22 | zip = ZipFile(inputFile) 23 | zip.extractall(pwd=password) 24 | return 25 | 26 | # Split the file into smaller chunks 27 | def splitFile(inputFile): 28 | #read the contents of the file 29 | f = open(inputFile, 'r') 30 | data = f.read() 31 | f.close() 32 | inputFile = inputFile.split(os.path.sep)[-1] 33 | #print 'original data len' + str(len(data)) 34 | #os.remove(inputFile[:-4]) 35 | #os.remove(inputFile) 36 | 37 | #create a info.txt file for writing metadata 38 | i = 0 39 | j = 0 40 | os.mkdir(inputFile + 'dir') 41 | 42 | if len(data) < CHUNK_SIZE: 43 | blockfile = open(inputFile + 'dir/' + inputFile + '0', 'w') 44 | blockfile.write(data) 45 | i += 1 46 | else: 47 | while (CHUNK_SIZE * i < len(data)): 48 | #print str(len(block)) + '\n' 49 | #blockfile = open(inputFile + 'dir/' + inputFile + str(i), 'wb') 50 | blockfile = open(inputFile + 'dir/' + inputFile + str(i), 'w') 51 | blockfile.write(data[CHUNK_SIZE * i:CHUNK_SIZE * (i + 1)]) 52 | #print 'line 40 blocksize: ' + str(len(''.join(block))) 53 | i += 1 54 | #print 'line 42: i is ', str(i) 55 | blockfile.close() 56 | if (CHUNK_SIZE * i + j < len(data)): 57 | #print 'overflowed!' 58 | blockfile = open(inputFile + 'dir/' + inputFile + str(i), 'w') 59 | #print 'i is ' + str(i) + '\n' 60 | #print 'CHUNK_SIZE is ' + str(CHUNK_SIZE) 61 | #print 'j is ' + str(j) 62 | #print 'len of data is ' + str(len(data)) 63 | #print 'so what is CHUNK_SIZE * i + j?', str(CHUNK_SIZE*i+j) 64 | while (((CHUNK_SIZE * i) + j) < len(data)): 65 | #print 'overflowed a bit\n' 66 | blockfile.write(data[CHUNK_SIZE * i + j]) 67 | j += 1 68 | blockfile.close() 69 | if (j > 0): i += 1 70 | infofile = open('info.txt', 'w') 71 | infofile.write(inputFile + ',' + str(i) + ',' + str(CHUNK_SIZE)) 72 | infofile.close() 73 | return 74 | 75 | #define the function to join the chunks of files into a single file 76 | def joinFiles(fileName, noOfChunks): 77 | # this function works 78 | data = [] 79 | for i in range(noOfChunks): # change this to check how many pieces are in the folder 80 | chunkName = fileName + '/' + fileName[:-3] + str(i) 81 | curChunk = open(chunkName, 'r') 82 | data.append(curChunk.read()) 83 | curChunk.close() 84 | shutil.rmtree(fileName) # delete folder of chunks 85 | joined = open(fileName, 'w') 86 | joined.write(''.join(data)) # write joined chunk file 87 | joined.close() 88 | return 89 | 90 | #splitFile('spim.png') 91 | #joinFiles('spim.png') 92 | 93 | ''' 94 | # python chunk.py -e file password 95 | if (len(sys.argv) == 4 and sys.argv[1] == "-e"): 96 | # make encrypted zip 97 | zipcrypt(sys.argv[2], sys.argv[2] + '.zip', sys.argv[3]) 98 | 99 | # call the file splitting function 100 | 101 | splitFile(sys.argv[2] + '.zip') 102 | 103 | elif (len(sys.argv) == 4 and sys.argv[1] == "-d"): 104 | # python chunk.py -d foldername password 105 | #call the function to join the splitted files 106 | f = open('info.txt') 107 | line = f.readline() 108 | f.close() 109 | num = line.split(',')[1] 110 | #print 'num', num 111 | joinFiles(sys.argv[2], int(num)) 112 | zipdecrypt(sys.argv[2], sys.argv[3]) 113 | else: 114 | print 'python chunk.py -e file password' 115 | print 'python chunk.py -d foldername password' 116 | ''' 117 | -------------------------------------------------------------------------------- /src/download.py: -------------------------------------------------------------------------------- 1 | #Download a file from a given URL 2 | 3 | import urllib2 4 | import os 5 | 6 | def fbdownload(url,file_name): 7 | 8 | # file_name = url.split('/')[-1] 9 | u = urllib2.urlopen(url) 10 | if not os.path.exists('downloads'): 11 | os.makedirs('downloads') 12 | f = open("downloads/" + file_name, 'wb') 13 | meta = u.info() 14 | file_size = int(meta.getheaders("Content-Length")[0]) 15 | print "Downloading: %s Bytes: %s" % (file_name, file_size) 16 | 17 | file_size_dl = 0 18 | block_sz = 8192 19 | while True: 20 | buffer = u.read(block_sz) 21 | if not buffer: 22 | break 23 | 24 | file_size_dl += len(buffer) 25 | f.write(buffer) 26 | status = r"%10d [%3.2f%%]" % (file_size_dl, file_size_dl * 100. / file_size) 27 | status = status + chr(8)*(len(status)+1) 28 | print status, 29 | 30 | f.close() 31 | 32 | #fbdownload("https://cdn.fbsbx.com/hphotos-xfp1/v/t59.2708-21/10971559_1377947505849040_1383162146_n.asm/test1.asm?oh=da6c58f5cb8935fd399b0bbecca64ca9&oe=54CF9C97&dl=1") 33 | -------------------------------------------------------------------------------- /src/encryption.py: -------------------------------------------------------------------------------- 1 | import base64 2 | from Crypto.Cipher import AES 3 | from Crypto import Random 4 | import os 5 | 6 | key = os.urandom(AES.block_size*2) 7 | BS = AES.block_size 8 | pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 9 | unpad = lambda s : s[:-ord(s[len(s)-1:])] 10 | def encrypt(raw): 11 | raw = pad(raw) 12 | iv = Random.new().read( AES.block_size ) 13 | cipher = AES.new(key, AES.MODE_CBC, iv ) 14 | return base64.b64encode( iv + cipher.encrypt( raw ) ) 15 | 16 | def decrypt(enc): 17 | enc = base64.b64decode(enc) 18 | iv = enc[:16] 19 | cipher = AES.new(key, AES.MODE_CBC, iv ) 20 | return unpad(cipher.decrypt( enc[16:] )) 21 | 22 | f= open('1.jpg') 23 | plain = f.read() 24 | cipher = encrypt(plain) 25 | 26 | fOut = open('encrypted','w') 27 | fOut.write(cipher) 28 | fOut.close() 29 | 30 | f= open('encrypted') 31 | cipher = f.read() 32 | decrypted = open('decrypted', 'w') 33 | decrypted.write(decrypt(cipher)) 34 | decrypted.close() 35 | 36 | -------------------------------------------------------------------------------- /src/fbIO.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import chunk 4 | import fbupload 5 | import os 6 | from selenium import webdriver 7 | 8 | def upload(filedir,username,password): 9 | chunk.zipcrypt(filedir, filedir + '.zip', password) 10 | chunk.splitFile(filedir + '.zip') 11 | 12 | chunks = os.listdir(filedir.split(os.path.sep)[-1] + '.zipdir') 13 | paths = [os.path.abspath(os.path.join(filedir + '.zipdir', part))\ 14 | for part in chunks] 15 | 16 | driver = fbupload.fbupload(paths, username, password) 17 | print 'returned from upload' 18 | urls = fbupload.fbdownload(filedir.split(os.path.sep)[-1], username, password, driver) 19 | print 'returned from download' 20 | return urls 21 | 22 | def retrieve(filename,username,password): 23 | #get info.txt, files 24 | f = open('info.txt') 25 | line = f.readline() 26 | f.close() 27 | fileName, noOfChunks, chunkSize = line.split(',') 28 | chunk.joinFiles(fileName,noOfChunks,chunkSize) 29 | chunk.zipdecrypt(filename,password) 30 | 31 | def link_name_map(): 32 | if (os.path.isfile('links.txt')): 33 | f=open('links.txt', 'r') 34 | links=f.read() 35 | f.close() 36 | return re.findall(r'\/([^?/]+)\?oh=', links), links.split('\n') 37 | else: 38 | return [] 39 | ''' 40 | 41 | filedir='textfile.txt' 42 | 43 | urls = upload(filedir, username, password) 44 | for url in urls: 45 | print url + '\n' 46 | 47 | #x=link_name_map() 48 | #print(x[0]) 49 | #print(x[1]) 50 | 51 | #upload(filedir,username,password) 52 | #retrive(filedir,username,password) 53 | 54 | ''' 55 | -------------------------------------------------------------------------------- /src/fbupload.py: -------------------------------------------------------------------------------- 1 | from selenium import webdriver 2 | from selenium.webdriver.common.keys import Keys 3 | import re 4 | import time 5 | import os 6 | from selenium.common.exceptions import TimeoutException 7 | from selenium.webdriver.support.ui import WebDriverWait 8 | from selenium.webdriver.support import expected_conditions as EC 9 | 10 | def fbupload(files, email, pword): 11 | # launch headless PhantomJS browser pointed at facebook 12 | # https://stackoverflow.com/a/23872305 13 | 14 | # creates a new Firefox process 15 | #driver = webdriver.PhantomJS() 16 | driver = webdriver.Firefox() 17 | 18 | driver.get('https://www.facebook.com') 19 | assert 'Facebook' in driver.title 20 | 21 | #https://stackoverflow.com/questions/25569426/unable-to-login-to-quora-using-selenium-webdriver-in-python 22 | form = driver.find_element_by_class_name("menu_login_container") 23 | username = form.find_element_by_name("email") 24 | username.send_keys(email) 25 | 26 | password = form.find_element_by_name("pass") 27 | password.send_keys(pword) 28 | password.send_keys(Keys.RETURN) 29 | time.sleep(2) 30 | 31 | # Open a Facebook 404 page to grab the username because it loads faster 32 | # than the newsfeed 33 | driver.get('https://www.facebook.com/aksdjsandkjasndjknandjsakndkjsads/') 34 | assert 'Page Not Found' in driver.title 35 | 36 | # get user ID from profile button 37 | ID = (driver.find_element_by_class_name('_2dpe').get_attribute('href') 38 | .split('/')[-1]) 39 | 40 | driver.get('https://www.facebook.com/messages/' + ID) 41 | time.sleep(2) 42 | assert 'Messages' in driver.title 43 | time.sleep(2) 44 | #driver.save_screenshot('screenshot.jpg') 45 | 46 | #tag = 'a._59hn' 47 | #btns = driver.find_elements_by_css_selector(tag) 48 | #links=open('links.txt','w') 49 | #for btn in btns: 50 | # links.write(btn.get_attribute('href')+'\n') 51 | #links.close() 52 | 53 | for fileDir in files: 54 | print 'sending file dir ' + os.path.join(os.getcwd(), 55 | fileDir.split(os.path.sep)[-2], fileDir.split(os.path.sep)[-1]) 56 | input = driver.find_element_by_class_name("_3jk") 57 | attach = input.find_element_by_name("attachment[]") 58 | time.sleep(2) 59 | attach.send_keys(os.path.join(os.getcwd(), 60 | fileDir.split(os.path.sep)[-2], fileDir.split(os.path.sep)[-1])) 61 | time.sleep(2) 62 | message = driver.find_element_by_class_name("_1rt") 63 | m = message.find_element_by_name("message_body") 64 | m.send_keys(Keys.ENTER) 65 | time.sleep(3) 66 | return driver 67 | 68 | def fbdownload(fileName, email, pword, driver=None): 69 | # launch headless PhantomJS browser pointed at facebook 70 | # https://stackoverflow.com/a/23872305 71 | 72 | if not driver: 73 | # creates a new PhantomJS process 74 | driver = webdriver.PhantomJS() 75 | #driver = webdriver.Firefox() 76 | 77 | driver.get('https://www.facebook.com') 78 | assert 'Facebook' in driver.title 79 | 80 | #https://stackoverflow.com/questions/25569426/unable-to-login-to-quora-using-selenium-webdriver-in-python 81 | form = driver.find_element_by_class_name("menu_login_container") 82 | username = form.find_element_by_name("email") 83 | username.send_keys(email) 84 | 85 | password = form.find_element_by_name("pass") 86 | password.send_keys(pword) 87 | password.send_keys(Keys.RETURN) 88 | 89 | assert 'Facebook' in driver.title 90 | 91 | # Open a Facebook 404 page to grab the username because it loads faster 92 | # than the newsfeed 93 | driver.get('https://www.facebook.com/aksdjsandkjasndjknandjsakndkjsads/') 94 | assert 'Page Not Found' in driver.title 95 | 96 | # get user ID from profile button 97 | ID = (driver.find_element_by_class_name('_2dpe').get_attribute('href') 98 | .split('/')[-1]) 99 | driver.get('https://www.facebook.com/messages/' + ID) 100 | 101 | assert 'Messages' in driver.title 102 | 103 | tag = 'a._59hn' 104 | btns = driver.find_elements_by_css_selector(tag) 105 | urls = [] 106 | 107 | time.sleep(2) 108 | 109 | # Append all result URLs to results 110 | for btn in btns: 111 | urls.append(btn.get_attribute('href')) 112 | 113 | print 'scraped', str(len(urls)), 'urls' 114 | #print urls[0] 115 | print 'pursuing matches for ', fileName 116 | 117 | # If URL matches filename, keep it in results 118 | results = filter(lambda match: fileName in match, urls) 119 | 120 | #print results[0] 121 | print 'scraped', str(len(results)), 'urls for', fileName 122 | 123 | f = open('links.txt', 'a') 124 | for link in results: 125 | f.write(link) 126 | f.write('\n') 127 | f.close() 128 | return sorted(results, 129 | key=lambda numerical: re.search(r'\/([^?/]+)\?oh=', numerical).group(1)) 130 | -------------------------------------------------------------------------------- /src/gui.py: -------------------------------------------------------------------------------- 1 | import wx 2 | 3 | from Tkinter import * 4 | from tkFileDialog import askopenfilename 5 | import Tkinter as tk 6 | import fbIO 7 | import wx.lib.agw.hyperlink as hl 8 | import chunk 9 | import sys 10 | 11 | class windowClass(wx.Frame): 12 | 13 | def __init__(self, *args, **kwargs): 14 | super(windowClass, self).__init__(*args, **kwargs) 15 | 16 | self.basicGUI() 17 | 18 | def upload(self, event, uname, pword): 19 | Tk().withdraw() 20 | filename=askopenfilename() 21 | 22 | fbIO.upload(filename, uname, pword) 23 | 24 | def basicGUI(self): 25 | 26 | #splash screen 27 | root = tk.Tk() 28 | 29 | root.overrideredirect(True) 30 | width = root.winfo_screenwidth() 31 | height = root.winfo_screenheight() 32 | root.geometry('%dx%d+%d+%d' % (width*0.5, height*0.5, width*0.1, height*0.1)) 33 | 34 | image_file = "res/faceboxx_logo.gif" 35 | 36 | image = tk.PhotoImage(file=image_file) 37 | canvas = tk.Canvas(root, height=height*0.8, width=width*0.8, bg="black") 38 | canvas.create_image(width*0.5/2, height*0.5/2, image=image) 39 | canvas.pack() 40 | 41 | # show the splash screen for 3000 milliseconds then destroy 42 | root.after(3000, root.destroy) 43 | root.mainloop() 44 | 45 | #start application 46 | panel = wx.Panel(self) 47 | 48 | 49 | 50 | ## LOGIN DIALOGS ## 51 | getName = wx.TextEntryDialog(None, 'Enter Facebook Login') 52 | 53 | if getName.ShowModal()==wx.ID_OK: 54 | userName = getName.GetValue() 55 | 56 | getPass = wx.PasswordEntryDialog(None,'Enter Password') 57 | 58 | if getPass.ShowModal()==wx.ID_OK: 59 | passWord = getPass.GetValue() 60 | 61 | button=wx.Button(panel, label='New Upload',pos=(300,150),size=(80,30)) 62 | self.Bind(wx.EVT_BUTTON, 63 | lambda event: self.upload(event, userName, passWord), 64 | button) 65 | 66 | menuBar = wx.MenuBar() 67 | 68 | fileButton = wx.Menu() 69 | exitItem = wx.MenuItem(fileButton, wx.ID_EXIT, 'Exit\tCtrl+Q') 70 | exitItem.SetBitmap(wx.Bitmap('res/sad_face.png')) 71 | fileButton.AppendItem(exitItem) 72 | 73 | menuBar.Append(fileButton, '&File') 74 | 75 | self.SetMenuBar(menuBar) 76 | self.Bind(wx.EVT_MENU, self.Quit, exitItem) 77 | 78 | self.SetTitle('Faceboxx') 79 | self.Show(True) 80 | 81 | 82 | 83 | ## FILE HEADERS ## 84 | 85 | ### change to be hidden until file is uploaded/split? or just don't show at all? 86 | 87 | Text1 = wx.StaticText(panel, -1, 'Filename', (10,10)) 88 | Text1.SetForegroundColour('black') 89 | 90 | Text2 = wx.StaticText(panel, -1, '#Files', (80,10)) 91 | Text2.SetForegroundColour('#black') 92 | 93 | Text3 = wx.StaticText(panel, -1, 'Filesize(mb)', (130,10)) 94 | Text3.SetForegroundColour('#black') 95 | 96 | Text4 = wx.StaticText(panel, -1, 'Date Uploaded', (210,10)) 97 | Text4.SetForegroundColour('#black') 98 | 99 | ## FILE LIST ## 100 | indata = fbIO.link_name_map() 101 | if indata: 102 | i = 0 103 | names = indata[0] 104 | link = indata[0] 105 | position = [10, 40] 106 | shown = [] 107 | #for k in xrange(len(indata[0])): 108 | #name = 'file' + str(i) 109 | #name = hl.HyperLinkCtrl(panel, -1, names[i], pos=position, 110 | # URL=link[i]) 111 | if names[i][:-1] not in shown: 112 | button=wx.Button(panel, label=names[i][:-1], pos=position,size=(80,30)) 113 | shown += names[i][:-1] 114 | i += 1 115 | position[1] += 30 116 | self.Bind(wx.EVT_BUTTON, self.combine, button) 117 | 118 | def Quit(self, e): 119 | self.Close() 120 | sys.exit(0) 121 | 122 | def combine(self, event): 123 | # get label name 124 | # iterate through links and download each file with matching name 125 | # to name.zipdir/. 126 | # call joinFiles(name.zipiter, number of chunks) 127 | # call zipdecrypt(inputfile, password) 128 | return 129 | 130 | def main(): 131 | app = wx.App() 132 | windowClass(None) 133 | 134 | app.MainLoop() 135 | return 136 | 137 | main() 138 | --------------------------------------------------------------------------------