Advertisement
s243a

rest_test5.dev

Nov 13th, 2018
245
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 21.00 KB | None | 0 0
  1. import bottle
  2. from bottle import route, run, Bottle, request
  3. import sys, os, re, urllib
  4. import datetime #I think I need this
  5. import time #I think I need this
  6. from gevent import queue
  7. from gevent import Greenlet
  8. import gevent
  9. import StringIO
  10. from datetime import datetime
  11. from bottle import response #https://stackoverflow.com/questions/17262170/bottle-py-enabling-cors-for-jquery-ajax-requests
  12.  
  13. #from gevent import pywsgi #Don't need this yet
  14. fcpHost = "127.0.0.1"
  15. import fcp
  16. #node = fcp.FCPNode(host=fcpHost, verbosity=fcp.DETAIL)
  17. jobs={}
  18. app = Bottle()
  19.  
  20. #https://stackoverflow.com/questions/17262170/bottle-py-enabling-cors-for-jquery-ajax-requests#17262900
  21. # the decorator
  22. def enable_cors(fn):
  23.     def _enable_cors(*args, **kwargs):
  24.         # set CORS headers
  25.         response.headers['Access-Control-Allow-Origin'] = '127.0.0.1'
  26.         response.headers['Vary'] = 'Origin'
  27.         response.headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, OPTIONS'
  28.         response.headers['Access-Control-Allow-Headers'] = 'Origin, Accept, Content-Type, X-Requested-With, X-CSRF-Token'
  29.  
  30.         if bottle.request.method != 'OPTIONS':
  31.             # actual request; reply with the actual response
  32.             return fn(*args, **kwargs)
  33.  
  34.     return _enable_cors
  35.  
  36.  
  37. def delim_filter(config):
  38.     bottle.debug(True)
  39.     print("Entering delim_filter")
  40.     ''' Matches a comma separated list of numbers. '''
  41.     aconfig = config or "%20"
  42.     print ("aconfig=" + aconfig)
  43.     delimiter = urllib.unquote(aconfig).decode('utf8')
  44.     print("delimiter=" + delimiter)  
  45.     regexp = r'(?!{a_delim})+({a_delim}(?!{a_delim})+)*'.format(a_delim=re.escape(delimiter))
  46.     print("regexp="+regexp)
  47.     def to_python(match):
  48.         print("Converting Match")
  49.         print("Math=" + match)
  50.         ms = map(urllib.unquote(aconfig).decode,match.split())
  51.         print( "ms=" + ms )
  52.         return ms
  53.  
  54.     def to_url(astr):
  55.         print("Converting to URL")
  56.         print ("astr=" + astr)
  57.         astr2 = delimiter.join(map(urllib.unquote(aconfig).decode,astr))
  58.         print("astr2="+astr2)
  59.         print  astr2
  60.         return astr2
  61.  
  62.     return regexp, to_python, to_url
  63.  
  64. app.router.add_filter('delim', delim_filter)
  65.  
  66. def split_urlArgs(url,delim=" ",
  67.                   decode='All',
  68.                   xfm_de=lambda a: urllib.unquote(a).decode('utf8'),
  69.                   xfm_split=lambda a, b: re.split(a,b), #Typically a is a regular expression and b a string
  70.                   fn_regexp = lambda a: r'(?!{a_delim})+({a_delim}(?!{a_delim})+)*'.format(a_delim=a) ):
  71.     bottle.debug(True)
  72.     ''' Matches a comma separated list of numbers. '''
  73.     print("delimiter=" + delim)  
  74.     if decode == "Args":
  75.         ms = xfm_split(fn_regexp(a),url)
  76.     elif decode == "All":
  77.         url2 = xfm_de(url)    #Decode the URL
  78.         delim= xfm_de(delim) #Decode the delimitor
  79.         ms = xfm_split(fn_regexp(delim),url)
  80.     elif decode == "None":
  81.         ms = xfm_split(fn_regexp(delim),url) #SPlit the URL based on a regular expression.
  82.            
  83.     print( "ms=" + "["+",".join(ms) +"]" )    
  84.     return ms, delim
  85.  
  86. def combine_urlArgs(args,delim=" ",
  87.                     encode='All',
  88.                     xfm_encode=lambda a: urllib.quote(a, safe=''),
  89.                     xfm_join=lambda a, b: a.join(b)): #Typically a is the delimiter and b is a list
  90.     print("Converting to URL")
  91.     print ("astr=" + args)
  92.     if decode == "Args":
  93.         astr2 = xfm_join(delim,(map(lambda a: xfm_encode,args)))
  94.     elif decode == "All":
  95.         astr2 = urllib.unquote(xfm_join(delim,args))
  96.     elif decode=="None":
  97.         astr2 = xfm_join(delim,args)      
  98.     print("astr2="+astr2)
  99.     return astr2    
  100.  
  101.  
  102. #@app.route('/<key>/%09/<cmd>/<args:delim:%09>') # %09 == tab
  103. #@app.route('/<key>/%20/<cmd>/<args:delim:%20>') # %20 == ' '
  104. #@app.route('/<key>/ /<cmd>/<args:delim: >') # %20 == ' '
  105. #@app.route('/<key>/%3B/<cmd>/<args:delim:%3B>') # %3B == ';'
  106. #@app.route('/<key>/;/<cmd>/<args:delim:;>') # %3B == ';'
  107. #@app.route('/<key>/%2C/<cmd>/<args:delim:%2C>') # %2C == ','
  108. #@app.route('/<key>/,/<cmd>/<args:delim:,>') # %2C == ','
  109. @app.route('/<key>/<sep>/<cmd>/<args:path>') # %2C == ','
  110. def mystery(key,cmd,args,sep=""):
  111.     bottle.debug(True)
  112.     print("engering mystery")
  113.     print("sep=" + sep)
  114.     if len(sep) > 0 : # True if seperator is a wild card
  115.         args, sep = split_urlArgs(args,sep)
  116.     if key == '1234': # %2C == ','
  117.         print(key)
  118.         print(cmd)
  119.         print(args)
  120.         #delimiter = urllib.unquote(aconfig).decode('utf8')
  121.         #print(delimiter)            
  122.         result1 = os.popen("IFS=<a_dilim>".format(a_dilim=sep)).read()
  123.         print("result1=" + result1)
  124.         result2 = result1 + "<br>"
  125.         print("result2=" + result2)    
  126.         cmd_str=cmd + sep + sep.join(args)
  127.         print(cmd_str)  
  128.         result3 = result2 +os.popen(cmd_str).read()
  129.         print("result3=" + result3)              
  130.         return "<pre>" + result3 + "</pre>"
  131.         #return result3.replace("\n","<br>")
  132.         #return ('key=' + key + '<br>' +
  133.         #     'sep=' + sep + '<br>' +
  134.         #     'cmd=' + cmd + '<br>' +
  135.         #     'args='   + "[" + ",".join(args)+"]")
  136. def generateFreeWorker(job_or_uri,message=None,afile=None,fcpHost = "127.0.0.1",fcpPort= 9481,aBody=None,OutputType="HTML"):
  137.     fw = FreeWorkerFactory(job_or_uri,message,afile,fcpHost,fcpPort,aBody,OutputType)
  138.     return fw        
  139. #class calbackExecuter:
  140. #    __init__(self,calbacks):
  141. #        self.callbacks=calbacks
  142. #    def execute():
  143. #        for c in self.calbacks():
  144. #            c()
  145. class loggerContainer:
  146.     def __init__(self,node=None,**kw):
  147.         self.ready = False
  148.         self.strBuff = StringIO.StringIO()
  149.         self.job = kw.get('job',None)
  150.         self.Brk = kw.get('Brk',"\n")
  151.         self.body = kw.get('body',None) #The html body to write to.
  152.         self.complete=False
  153.         self.msgDict={}
  154.         self.meseges=[None]*5
  155.         self.msgPosNext=0
  156.         self.msgPos=-1
  157.         self.msgMax=5
  158.         self.msgLnCnt=0  
  159.         self.node=kw.get('node',None)
  160.     def filterMSG(self,msg): #We don't want to see the same log message too often
  161.         t1=self.msgDict.get(msg,None)
  162.         if t1 is not None: #isinstance(t1,datetime):
  163.             t2=datetime.now()
  164.             delta = t2 - t1 #https://stackoverflow.com/questions/2880713/time-difference-in-seconds-as-a-floating-point
  165.             if delta.seconds>10:
  166.                 return True
  167.             else:
  168.                 return False
  169.         return True
  170.     def do_logfunc(self,msgline,cb=None):
  171.         #print("self.msgPosNext="+str(self.msgPosNext))
  172.         self.meseges[self.msgPosNext]={ "msg_str" : msgline,
  173.                                         "time" : datetime.now()
  174.                                       }      
  175.         self.msgPos=self.msgPosNext
  176.         #print("self.msgMax="+str(self.msgMax))
  177.         self.msgPosNext=((self.msgPosNext+1)%(self.msgMax))
  178.         self.msgDict[msgline]=datetime.now()
  179.         if self.msgLnCnt<self.msgMax:
  180.             self.msgLnCnt=self.msgLnCnt+1        
  181.     def logfunc(self,msgline,cb=None,HTML_lb="<br>"):
  182.         mDict=self.parseMsg(msgline)
  183.         #if cb is None:
  184.         #    cb=lambda msg: self.print_and_put_cb(msg,HTML_lb="<br>",chkSender=True)
  185.         self.do_logfunc(msgline,cb)    
  186.         if cb is None:
  187.             self.print_and_put_cb(msgline,mDict,HTML_lb="<br>",chkSender=False)
  188.         else:
  189.             cb(msgline,mDict,HTML_lb="<br>")
  190.     def parseMsg(self,msg):
  191.         mDict=self.parseSenderLine(msg)
  192.         sender=mDict.get('sender',None)
  193.         if (sender == 'CLIENT') or (sender == 'CLIENT'):
  194.             mDict['node_or_client']=True
  195.             try: #Not sure if we need a try/except here
  196.                 #k, v = mgStrToKey(mDict['msg_str'])
  197.                 #mDict['k']=v
  198.                 mDict=self.parseFCPMsgStr(mDict['msg_str']) # I haven't decided if a dictionary should be returned or a seperate key/value
  199.             except:
  200.                 pass
  201.         return mDict    
  202.     def parseSenderLine(self,msg_line):
  203.        regex=r'^(?P<sender>\S+)[:]\s+(?P<msg_str>[^\n]+)\n{0,1}$'
  204.        try: #Not sure if we need a try/except here
  205.            #print msg_line
  206.            m=re.match(regex,msg_line)
  207.            mDict=m.groupdict()
  208.            assert len(mDict['msg_str'])>0 #,"Could not parse msg_str"
  209.        except AssertionError:
  210.            print("Could not parse msg_str")
  211.            print("msg_line="+msg_line)
  212.            return mDict
  213.        if mDict is None:
  214.            mDict={'sender' : 'Unknown',
  215.                  'msg_str' : msg_line }
  216.        return mDict
  217.     def parseFCPMsgStr(self,msg_line): #I should think of a better name for this
  218.         mDict, succeeded = self.parseSenderLine(msg_line)
  219.         if succeeded:
  220.             k, v, hasKeyVal = line.split("=", 1)
  221.             if hasKeyVal:
  222.                 mDict['parsed'][k]=v
  223.             else:
  224.                 mDict['parsed'][mDict['msg_str']]=True #Maybe do some checks here such as length and for white space characters
  225.         return mDict, succeeded
  226.     def print_and_put_cb(self,msg,mDict,HTML_lb="<br>",chkSender=True):
  227.         node_or_client=mDict.get('node_or_client',False)
  228.         #chkSender= not node_or_client
  229.         if self.filterMSG(msg): #Don't reprint the same message if we've scene it recently.
  230.             print self.msg_to_str(msg)
  231.         if not node_or_client:
  232.             self.bodyPut_cb(msg,HTML_lb=HTML_lb,chkSender=False)
  233.     def bodyPut_cb(self,msg,HTML_lb="<br>",chkSender=True):
  234.         #parseMsg
  235.         if chkSender:
  236.             if self.filterForBody(msg):
  237.                 print('bodyPut_cb: This branch should not normally be called')
  238.                 self.body.put(self.msg_to_str(msg)+HTML_lb)
  239.         else:
  240.             self.body.put(self.msg_to_str(msg)+HTML_lb)
  241.            
  242.     def filterForBody(self,msg):
  243.         if msg.startswith("NODE:") or msg.startswith("CLIENT:"):
  244.             return True
  245.         else:
  246.             return False
  247.  
  248.     def detectEndMsg(self,msg):
  249.        
  250.         if filterForBody(self,msg):
  251.             node_or_client = True
  252.         else:
  253.             node_or_client = False
  254.         return node_or_client
  255.        
  256.  
  257.     def msg_to_str(self,msg):
  258.         if type(msg) == str:
  259.             return msg
  260.         elif type(msg) is dict: #isinstance(msg, collections.Mapping): #https://stackoverflow.com/questions/25231989/how-to-check-if-a-variable-is-a-dictionary-in-python
  261.             return msg['msg_str']
  262.         else:
  263.             return "Empty Message"
  264.     def tail(self,lines=0,cb=None,delta=None):
  265.         if cb is None:
  266.             cb=self.print_and_put_cb
  267.         if lines == 0:
  268.             N=self.msgMax
  269.         else:
  270.             N=min(self.msgMax,lines)
  271.         for i in range(self.msgPost,self.msgPos-N+1,-1):
  272.             msg_i=self.meseges[i%self.msgMax]
  273.             if delta is not None:
  274.                 t2=datetime.now()
  275.                 t1=msg_i['time']
  276.                 delta2=t2-t1
  277.                 if delta2.delta.microseconds > delta.microseconds:
  278.                     break
  279.             cb(self.meseges[i%self.msgMax])
  280.                  
  281.         #self.body.put(msgline)  
  282.     def myLogger(self, level, msg):
  283.         print("level="+level + " msg=" + msg)
  284.         if level > self.job.verbosity:
  285.             return  
  286.         if not msg.endswith(self.Brk):
  287.             msg += "\n"
  288.        
  289.         strBuff.write(msg)
  290.         self.isReady = True
  291.         #strBuff.flush()
  292.         #time.sleep(0.001)
  293.     def getLogger(self): #THis might be redundant
  294.         return self.myLogger              
  295.     def isReady(self):
  296.         return self.ready
  297.     def flush(self):
  298.         val=self.strBuff.getvalue()
  299.         self.strBuff.close()
  300.         self.strBuff = StringIO.StringIO()
  301.         self.isRead=False
  302.         return val
  303.     def getValue(self):
  304.         self.strBuff.getValue()
  305.     def readBuf(self):
  306.         if self.ready:
  307.             val = self.flush()
  308.             ready = True
  309.             return val, ready
  310.         else:
  311.             ready = False
  312.             val = ""
  313.             return val, ready
  314.         return val, ready
  315.     def isComplete(self): # We might also want to check for an EndMessage and wait if needed
  316.         return self.complete
  317.         #if self.job.msg['hdr'] == 'PutSuccessful':
  318.         #    return True
  319.         #elif self.job.msg['hdr'] == 'PutFailed':
  320.         #    return True
  321.     def callback(self, status, value):
  322.         if status == 'PutSuccessful':
  323.             self.complete=True
  324.         elif status == 'PutFailed':
  325.             self.complete=True
  326.         #elif status == 'URIGenerated':
  327.         #    self.complete=True        
  328.         #self.msg=value
  329.     def getCallback(self):
  330.         return self.callback    
  331. class FreeWorkerFactory:
  332.  
  333.     #self.job = None #A freenet job
  334.     #self.g = None
  335.     #self.body = None
  336.     #self.OutputType =None
  337.    
  338.     def __init__(self,job_or_uri=None,**kw):
  339.         self.kw={}
  340.         self.kw['message']=kw.get('message',None)
  341.         self.kw['file']=kw.get(file,None)
  342.         self.loggerContainer=kw.get('loggerContainer',None)
  343.         #if self.loggerContainer is not None and self.node is not None:
  344.         #    self.loggerContainer.node=node
  345.         self.host=kw.get('host',"127.0.0.1")
  346.         self.port=self.kw.get('port',9481)
  347.         self.OutputType=self.kw.get('OutputType','HTML')
  348.         #selffcpPort,aBody,OutputType
  349.         if job_or_uri is not None:
  350.             if type(job_or_uri) is str:
  351.                 print("__init__" + "uri as input")
  352.                 if job_or_uri in jobs:                
  353.                     print("uri in jobs")
  354.                     self.job=jobs[job_or_uri]
  355.                 else:
  356.                     print("making job")
  357.                     node = fcp.FCPNode(fcpHost,verbosity='fcp.DETAIL',port=self.kw)
  358.                     val = self.kw['message']
  359.                     ksk = job_or_uri
  360.                     uri = "KSK@" + ksk
  361.                     node.put("KSK@"+ksk, data=val, mimetype="text/plain", verbosity=fcp.DETAIL)
  362.                     #self.job = node.get(uri, async=True)
  363.                     self.startCB = lambda: node.get(uri, async=True) #A start callback is safer than immediatly starting the job
  364.             else:
  365.                 print("__init__"+"job as input")
  366.                 print("Warning: Make sure that logger is created before job is started")
  367.                 self.job=job_or_uri
  368.                 self.startCB = lambda: None #Best of luck with this! Job might proceed before logger is ready.
  369.         if self.OutputType == "HTML":
  370.             self.Brk="<br>"
  371.         elif self.OutputType == "UNIX":
  372.             self.Brk="" #We won't use "\n" since print automatically generates the \n. Write can be used instead if one wants to explicitly use the /n
  373.         self.loggerContainer=kw.get('loggerContainer',None)
  374.         self.body=kw.get('body',None)
  375.         if self.body is None:
  376.             try:
  377.                 self.body = self.loggerContainer.body  
  378.             except:
  379.                 print("Warning: body should be set in logger before freenet worker is created")
  380.                 self.body = queue.Queue()
  381.         self.greenlet=kw.get('greenlet',None)
  382.         if self.greenlet is None:
  383.             g = Greenlet(self.on_data)
  384.         try:
  385.             if self.loggerContainer.body is None:
  386.                 if self.body is not None:
  387.                     self.loggerContainer.body=self.body
  388.         except:
  389.             pass
  390.  
  391.         #self.setBody(kw.get('body',None)) # Replaced w/ the above code
  392.         self.startCB=kw.get('startCB',None) #Maybe I need to define a default start CB.    
  393.        
  394.     def on_data(self):
  395.         # we can poll the job
  396.         while True:
  397.             val, ready = self.loggerContainer.readBuf()
  398.             if ready:
  399.                 print val
  400.                 self.body.put(val)    
  401.             if self.loggerContainer.isComplete():
  402.                 print "Yay! job complete"
  403.                 self.body.put(job.getResult()) #Returns the completion message.
  404.                 break        
  405.             else:
  406.                 print "Waiting" + datetime.now().strftime('%s(s) %m/%d/%Y')+self.Brk
  407.                 #self.body.put("Waiting" + datetime.now().strftime('%s(s) %m/%d/%Y')+self.Brk)
  408.                 time.sleep(0.001) #Yield control to other threads
  409.                 gevent.sleep(5) #This yields control to another greenlet
  410.         print("Apparently we are done!")
  411.         self.on_finish() #Maybe we can sequence the greenlets instead of putting this here.
  412.        
  413.     def on_finish(self):
  414.         self.body.put('</body></html>') #There should be a way to seperate these last two lines from this method.
  415.         self.body.put(StopIteration)
  416.     def header(self,start_response=None):
  417.         if self.OutputType == "HTML":
  418.             if start_response is not None:
  419.                 start_response('200 OK', [('Content-Type', 'text/html')])
  420.         self.body.put(' ' * 1000)
  421.         if self.OutputType == "HTML":
  422.             self.body.put("<html><body><h1>Current Time:</h1>")  
  423.            
  424.     def handle(self,environ=None, start_response=None,aBody=None):
  425.         lambda a: self.start(self,enviorn,start_response)
  426.         #g = Greenlet.spawn(on_data, body)
  427.     def setBody(self,aBody=None,aGreenlet=None):
  428.         if aBody is None:
  429.            if self.body is None:
  430.                print("setting body & greenlet. Normally we shouldn't do this here")
  431.                self.body = queue.Queue()
  432.                self.greenlet = Greenlet(self.on_data)
  433.                self.loggerContainer.body=self.body
  434.         else:
  435.            self.body = aBody
  436.         if aGreenlet is None:
  437.             if self.greenlet is None:
  438.                 self.greenlet = Greenlet(self.on_data)
  439.         else:
  440.             self.greenlet=aGreenlet
  441.         if self.loggerContainer.body is None:
  442.             self.loggerContainer.body=self.body
  443.     def start(self,environ=None, start_response=None,aBody=None,aGreenlet=None): # Not sure
  444.         #https://stackoverflow.com/questions/20824218/how-to-implement-someasyncworker-from-bottle-asynchronous-primer
  445.         #g = Greenlet.spawn(current_time, lambda a: on_data(body))
  446.         print("Set Body")
  447.         self.setBody(aBody)
  448.         print("header")
  449.         self.header(start_response)
  450.         self.startCB() #Maybe add the option for some key words (i.e. kw) to the startCB function
  451.         print("start")
  452.         self.greenlet.start()            
  453.         return self.body, self.greenlet
  454.  
  455. #@enable_cors
  456.        
  457. @app.post('/KSK/insert')
  458. @enable_cors
  459. def rest_test():
  460.     fcpHost=request.json.get('host','127.0.0.1')
  461.     fcpVerbosity=request.json.get('verbosity',fcp.NOISY)
  462.     fcpPort=request.json.get('port',9481)
  463.    
  464.  
  465.  
  466.    
  467.     val = request.json.get('message')
  468.     ksk = request.json.get('ksk')
  469.     uri = "KSK@" + ksk
  470.    
  471.     print "Inserting %s, containing '%s'" % (uri, val)
  472.     # do the put - note that 'data=' inserts a string directly
  473.     # note too that mimetype is optional, defaulting to text/plain
  474.     #node.put("KSK@"+ksk, data=val, mimetype="text/plain")
  475.     # ------------------------------------------
  476.     # now, demonstrate asynchronous requests    
  477.     print "Launching asynchronous request"
  478.     body = queue.Queue()    
  479.     lgrCnt=loggerContainer(body=body)    
  480.  
  481.     node = fcp.FCPNode(host=fcpHost,verbosity=fcp.NOISY,port=fcpPort,logfunc=lgrCnt.logfunc)    
  482.    
  483.  
  484.  
  485.     loggerContainer.node=node # We don't use this yet    
  486.  
  487.     aStartCB = lambda: node.put(uri, data=val, async=True)  #Can I start a thread with a lambda expression?      
  488.     worker = FreeWorkerFactory(loggerContainer=lgrCnt,startCB=aStartCB,body=body)
  489.     #g = Greenlet(worker.on_data) Due to dependencies create this in the worker factory.
  490.     #lgrCnt.greenlet=g  
  491.  
  492.  
  493.  
  494.  
  495.  
  496.    
  497. #    loggerContainer.job=job
  498. #    loggerContainer.node=node
  499. #    jobs["KSK@"+ksk]=job    
  500.  
  501. #    #node.logfunc=lgrCnt.logfunc
  502. #    job._log=lgrCnt.getLogger()
  503. #    job.callback=lgrCnt.getCallback()
  504.    
  505.    
  506.     body, g = worker.start() #Returns the greenelet
  507.     #body = gevent.queue.Queue()    
  508.     #worker = getFreenetWorker(job)
  509.     #worker.on_data(body.put)
  510.     #Finish https://stackoverflow.com/questions/20824218/how-to-implement-someasyncworker-from-bottle-asynchronous-primer
  511.     #worker.on_finish(lambda: body.put(StopIteration)) #    body.put('</body></html>')      
  512.  
  513.     return body      
  514.     #yield job_waitt("KSK@"+ksk)
  515.     #return "KSK Inserted" # We probably want to do wsomething smarter than this.
  516.    
  517.     # or we can await its completion
  518.  
  519. #@app.route('/USK@<hashKey:nocomma>,<ecryptionKey:nocomma>,<encryption:nocomma>/<metaData:path>')
  520. #def hello(hashKey,ecryptionKey,encryption,metaData):
  521. #    
  522. #    mimetype, val1 = node.get(uri)
  523. #    return ('hashKey=' + hashKey + '<br>' +
  524. #             'ecryptionKey=' + ecryptionKey + '<br>' +
  525. #             'encryption=' + encryption + '<br>' +
  526. #             'metaData='   + metaData)
  527. #            
  528. #Regular eressions use a seperate filter; https://bottlepy.org/docs/dev/routing.html
  529. app.run(host='localhost', port=8082, debug=True)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement