forked from endorphant/ttbp
		
	mail and code cleanup
_ttbp.py is now the working copy i'm using, so i don't bust the live program. i brought over the id code generator that i use in txtminebot to generate random id tags for feedback subject lines. the feedback mechanism now uses sendmail instead of writing to disk. i also started to clean up some of the larger text blocks by using here documents.
This commit is contained in:
		
							parent
							
								
									a82c25840e
								
							
						
					
					
						commit
						cbbdb1e8b7
					
				
							
								
								
									
										519
									
								
								bin/_ttbp.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										519
									
								
								bin/_ttbp.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,519 @@ | ||||
| #!/usr/bin/python | ||||
| 
 | ||||
| import os | ||||
| import random | ||||
| import tempfile | ||||
| import subprocess | ||||
| import time | ||||
| import json | ||||
| from email.mime.text import MIMEText; | ||||
| 
 | ||||
| import core | ||||
| import chatter | ||||
| import inflect | ||||
| import util | ||||
| 
 | ||||
| ## system globals | ||||
| SOURCE = os.path.join("/home", "endorphant", "projects", "ttbp", "bin") | ||||
| LIVE = "http://tilde.town/~" | ||||
| FEEDBACK = os.path.join("/home", "endorphant", "ttbp-mail") | ||||
| FEEDBOX = "endorphant@tilde.town" | ||||
| USERFILE = os.path.join("/home", "endorphant", "projects", "ttbp", "users.txt") | ||||
| p = inflect.engine() | ||||
| 
 | ||||
| ## user globals | ||||
| USER = os.path.basename(os.path.expanduser("~")) | ||||
| PATH = os.path.join("/home", USER, ".ttbp") | ||||
| PUBLIC = os.path.join("/home", USER, "public_html") | ||||
| WWW = os.path.join(PATH, "www") | ||||
| CONFIG = os.path.join(PATH, "config") | ||||
| TTBPRC = os.path.join(CONFIG, "ttbprc") | ||||
| DATA = os.path.join(PATH, "entries") | ||||
| SETTINGS = { | ||||
|         "editor": "none", | ||||
|         "publish dir": False | ||||
|     } | ||||
| 
 | ||||
| ## ui globals | ||||
| BANNER = open(os.path.join(SOURCE, "config", "banner.txt")).read() | ||||
| SPACER = "\n\n\n" | ||||
| INVALID = "please pick a number from the list of options!\n\n" | ||||
| DUST = "sorry about the dust, but this part is still under construction. check back later!\n\n" | ||||
| QUITS = ['exit', 'quit', 'q', 'x'] | ||||
| BACKS = ['back', 'b', 'q'] | ||||
| EJECT = "eject button fired! going home now." | ||||
| 
 | ||||
| ## ref | ||||
| 
 | ||||
| EDITORS = ["vim", "vi", "emacs", "pico", "nano"] | ||||
| SUBJECTS = ["help request", "bug report", "feature suggestion", "general comment"] | ||||
| 
 | ||||
| ## | ||||
| 
 | ||||
| def redraw(leftover=""): | ||||
|     os.system("clear") | ||||
|     print(BANNER) | ||||
|     print(SPACER) | ||||
|     if leftover: | ||||
|         print("> "+leftover+"\n") | ||||
| 
 | ||||
| def start(): | ||||
|   redraw() | ||||
|   #print(chatter.say("greet")+", "+chatter.say("friend")) | ||||
|   #print("(remember, you can always press ctrl-c to come home)\n") | ||||
|   print("if you don't want to be here at any point, press <ctrl-d> and it'll all go away.\njust keep in mind that you might lose anything you've started here.\n") | ||||
|   try: | ||||
|     print(check_init()) | ||||
|   except EOFError: | ||||
|       print(stop()) | ||||
|       return | ||||
| 
 | ||||
|   redraw() | ||||
| 
 | ||||
|   while 1: | ||||
|     try: | ||||
|         print(main_menu()) | ||||
|     except EOFError: | ||||
|         print(stop()) | ||||
|         break | ||||
|     except KeyboardInterrupt: | ||||
|         redraw(EJECT) | ||||
|     else: | ||||
|         break | ||||
| 
 | ||||
| def stop(): | ||||
|   return "\n\n\t"+chatter.say("bye")+"\n\n" | ||||
| 
 | ||||
| def check_init(): | ||||
|   global SETTINGS | ||||
|   print("\n\n") | ||||
|   if os.path.exists(os.path.join(os.path.expanduser("~"),".ttbp")): | ||||
|       print(chatter.say("greet")+", "+USER+".") | ||||
|       while not os.path.isfile(TTBPRC): | ||||
|         setup_handler() | ||||
|       try: | ||||
|         SETTINGS = json.load(open(TTBPRC)) | ||||
|       except ValueError: | ||||
|         setup_handler() | ||||
| 
 | ||||
|       raw_input("\n\npress <enter> to explore your feels.\n\n") | ||||
|       core.load() | ||||
|       return "" | ||||
|   else: | ||||
|     return init() | ||||
| 
 | ||||
| def init(): | ||||
|     try: | ||||
|         raw_input("i don't recognize you, stranger. let's make friends.\n\npress <enter> to begin, or <ctrl-c> to get out of here. \n\n") | ||||
|     except KeyboardInterrupt: | ||||
|         print("\n\nthanks for checking in! i'll always be here.\n\n") | ||||
|         quit() | ||||
| 
 | ||||
|     users = open(USERFILE, 'a') | ||||
|     users.write(USER+"\n") | ||||
|     users.close() | ||||
|     subprocess.call(["mkdir", PATH]) | ||||
|     subprocess.call(["mkdir", CONFIG]) | ||||
|     subprocess.call(["mkdir", DATA]) | ||||
|     #subprocess.call(["cp", os.path.join(SOURCE, "config", "defaults", "header.txt"), CONFIG]) | ||||
|     header = gen_header() | ||||
|     headerfile = open(os.path.join(CONFIG, "header.txt"), 'w') | ||||
|     for line in header: | ||||
|         headerfile.write(line) | ||||
|     headerfile.close() | ||||
|     subprocess.call(["cp", os.path.join(SOURCE, "config", "defaults", "footer.txt"), CONFIG]) | ||||
| 
 | ||||
|     setup() | ||||
|     subprocess.call(["cp", os.path.join(SOURCE, "config", "defaults", "style.css"), WWW]) | ||||
|     core.load() | ||||
| 
 | ||||
|     raw_input("\nyou're all good to go, "+chatter.say("friend")+"! hit <enter> to continue.\n\n") | ||||
|     return "" | ||||
| 
 | ||||
| def gen_header(): | ||||
|     header = [] | ||||
| 
 | ||||
|     header.append("<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 3.2//EN\">") | ||||
|     header.append("\n<html>") | ||||
|     header.append("\n\t<head>") | ||||
|     header.append("\n\t\t<title>~"+USER+" on TTBP</title>") | ||||
|     header.append("\n\t\t<link rel=\"stylesheet\" href=\"style.css\" />") | ||||
|     header.append("\n\t</head>") | ||||
|     header.append("\n\t<body>") | ||||
|     header.append("\n\t\t<div id=\"meta\">") | ||||
|     header.append("\n\t\t\t<h1><a href=\"index.html#\">~"+USER+"</a>@<a href=\"/~endorphant/ttbp\">TTBP</a></h1>") | ||||
|     header.append("\n\t\t</div>\n") | ||||
|     header.append("\n\t\t<!---put your custom html here-->\n\n\n\n") | ||||
|     header.append("\n\t\t<!---don't put anything after this line-->\n") | ||||
|     header.append("\n\t\t<div id=\"tlogs\">\n") | ||||
|     return header | ||||
| 
 | ||||
| def setup_handler(): | ||||
|     print("\nyour ttbp configuration doesn't look right. let's make you a fresh copy.\n\n") | ||||
|     try: | ||||
|         setup() | ||||
|     except KeyboardInterrupt: | ||||
|         print("\n\nsorry, trying again.\n\n") | ||||
|         setup() | ||||
| 
 | ||||
| def setup(): | ||||
|     global SETTINGS | ||||
| 
 | ||||
|     # editor selection | ||||
|     print_menu(EDITORS) | ||||
|     choice = raw_input("\npick your favorite text editor: ") | ||||
|     while choice  not in ['0', '1', '2', '3', '4']: | ||||
|         choice = raw_input("\nplease pick a number from the list: ") | ||||
| 
 | ||||
|     SETTINGS["editor"] = EDITORS[int(choice)] | ||||
|     redraw("text editor set to: "+SETTINGS["editor"]) | ||||
| 
 | ||||
|     # publish directory selection | ||||
|     if SETTINGS["publish dir"]: | ||||
|         print("\tcurrent publish dir:\t"+os.path.join(PUBLIC, SETTINGS["publish dir"])+"\n\n") | ||||
|     choice = raw_input("\nwhere do you want your blog published? (leave blank to use default \"blog\") ") | ||||
|     if not choice: | ||||
|         choice = "blog" | ||||
| 
 | ||||
|     publishing = os.path.join(PUBLIC, choice) | ||||
|     while os.path.exists(publishing): | ||||
|         second = raw_input("\n"+publishing+" already exists!\nif you're sure you want to use it, hit <enter> to confirm. otherwise, pick another location: ") | ||||
|         if second == "": | ||||
|             break | ||||
|         choice = second | ||||
|         publishing = os.path.join(PUBLIC, choice) | ||||
| 
 | ||||
|     SETTINGS["publish dir"] = choice | ||||
| 
 | ||||
|     # set up publish directory | ||||
|     if not os.path.exists(publishing): | ||||
|         subprocess.call(["mkdir", publishing]) | ||||
|         subprocess.call(["touch", os.path.join(publishing, "index.html")]) | ||||
|         index = open(os.path.join(publishing, "index.html"), "w") | ||||
|         index.write("<h1>ttbp blog placeholder</h1>") | ||||
|         index.close() | ||||
|     if os.path.exists(WWW): | ||||
|         subprocess.call(["rm", WWW]) | ||||
|     subprocess.call(["ln", "-s", publishing, WWW]) | ||||
|     print("\n\tpublishing to "+LIVE+USER+"/"+SETTINGS["publish dir"]+"/\n\n") | ||||
| 
 | ||||
|     # save settings | ||||
|     ttbprc = open(TTBPRC, "w") | ||||
|     ttbprc.write(json.dumps(SETTINGS, sort_keys=True, indent=2, separators=(',',':'))) | ||||
|     ttbprc.close() | ||||
| 
 | ||||
|     return SETTINGS | ||||
| 
 | ||||
| ## menus | ||||
| 
 | ||||
| def print_menu(menu): | ||||
|     i = 0 | ||||
|     for x in menu: | ||||
|         line = [] | ||||
|         line.append("\t[ ") | ||||
|         if i < 10: | ||||
|             line.append(" ") | ||||
|         line.append(str(i)+" ] "+x) | ||||
|         print("".join(line)) | ||||
|         i += 1 | ||||
| 
 | ||||
| def main_menu(): | ||||
|     menuOptions = [ | ||||
|             "record your feels", | ||||
|             "review your feels", | ||||
|             "check out your neighbors", | ||||
|             "browse global feels", | ||||
|             "change your settings", | ||||
|             "send some feedback", | ||||
|             "see credits"] | ||||
|     print("you're at ttbp home. remember, you can always press <ctrl-c> to come back here.\n\n") | ||||
|     #print("you're at ttbp home.\n\n") | ||||
|     print_menu(menuOptions) | ||||
| 
 | ||||
|     try: | ||||
|         choice = raw_input("\ntell me about your feels (or 'quit' to exit): ") | ||||
|     except KeyboardInterrupt: | ||||
|         redraw(EJECT) | ||||
|         return main_menu() | ||||
| 
 | ||||
|     if choice == '0': | ||||
|         redraw() | ||||
|         today = time.strftime("%Y%m%d") | ||||
|         write_entry(os.path.join(DATA, today+".txt")) | ||||
|     elif choice == '1': | ||||
|         redraw("here are your recorded feels, listed by date:\n\n") | ||||
|         view_own() | ||||
|     elif choice == '2': | ||||
|         users = find_ttbps() | ||||
|         redraw("the following "+p.no("user", len(users))+" "+p.plural("is", len(users))+" recording feels on ttbp:\n\n") | ||||
|         view_neighbors(users) | ||||
|     elif choice == '3': | ||||
|         redraw("now viewing most recent entries\n\n") | ||||
|         view_feed() | ||||
|     elif choice == '4': | ||||
|         pretty_settings = "\n\ttext editor:\t" +SETTINGS["editor"] | ||||
|         pretty_settings += "\n\tpublish dir:\t" +os.path.join(PUBLIC, SETTINGS["publish dir"]) | ||||
| 
 | ||||
|         redraw("now changing your settings. press <ctrl-c> if you didn't mean to do this.\n\ncurrent settings "+pretty_settings+"\n") | ||||
|         try: | ||||
|             setup() | ||||
|         except KeyboardInterrupt(): | ||||
|             redraw(EJECT) | ||||
|         raw_input("\nyou're all good to go, "+chatter.say("friend")+"! hit <enter> to continue.\n\n") | ||||
|         redraw() | ||||
|     elif choice == '5': | ||||
|         redraw() | ||||
|         feedback_menu() | ||||
|     elif choice == '6': | ||||
|         redraw() | ||||
|         show_credits() | ||||
|     elif choice in QUITS: | ||||
|         return stop() | ||||
|     else: | ||||
|         redraw(INVALID) | ||||
| 
 | ||||
|     return main_menu() | ||||
| 
 | ||||
| ### | ||||
| 
 | ||||
| def feedback_menu(): | ||||
|     print("you're about to send mail to ~endorphant about ttbp\n\n") | ||||
| 
 | ||||
|     print_menu(SUBJECTS) | ||||
|     choice = raw_input("\npick a category for your feedback: ") | ||||
| 
 | ||||
|     cat = "" | ||||
|     if choice in ['0', '1', '2', '3']: | ||||
|         cat = SUBJECTS[int(choice)] | ||||
|         entered = raw_input("\ncomposing a "+cat+" to ~endorphant.\n\npress <enter> to open an external text editor. mail will be sent once you save and quit.\n") | ||||
|         redraw(send_feedback(entered, cat)) | ||||
|         return | ||||
|     else: | ||||
|         redraw(INVALID) | ||||
| 
 | ||||
|     return feedback_menu() | ||||
| 
 | ||||
| def view_neighbors(users): | ||||
| 
 | ||||
|     userList = [] | ||||
| 
 | ||||
|     for user in users: | ||||
|         userRC = json.load(open(os.path.join("/home", user, ".ttbp", "config", "ttbprc"))) | ||||
|         url = LIVE+user+"/"+userRC["publish dir"] | ||||
|         count = 0 | ||||
|         lastfile = "" | ||||
|         files = os.listdir(os.path.join("/home", user, ".ttbp", "entries")) | ||||
|         files.sort() | ||||
|         for filename in files: | ||||
|             #if os.path.splitext(filename)[1] == ".txt" and len(os.path.splitext(filename)[0]) == 8: | ||||
|             if core.valid(filename): | ||||
|                 count += 1 | ||||
|                 lastfile = os.path.join("/home", user, ".ttbp", "entries", filename) | ||||
| 
 | ||||
|         ago = "never" | ||||
|         if lastfile: | ||||
|             last = os.path.getctime(lastfile) | ||||
|             since = time.time()-last | ||||
|             ago = util.pretty_time(int(since)) + " ago" | ||||
|         else: | ||||
|             last = 0 | ||||
| 
 | ||||
|         pad = "" | ||||
|         if len(user) < 8: | ||||
|             pad = "\t" | ||||
|         user = "~"+user | ||||
|         if len(user) < 8: | ||||
|             user += "\t" | ||||
| 
 | ||||
|         userList.append(["\t"+user+"\t"+url+pad+"\t("+ago+")", last]) | ||||
| 
 | ||||
|     # sort user by most recent entry | ||||
|     userList.sort(key = lambda userdata:userdata[1]) | ||||
|     userList.reverse() | ||||
|     sortedUsers = [] | ||||
|     for user in userList: | ||||
|         sortedUsers.append(user[0]) | ||||
| 
 | ||||
|     print_menu(sortedUsers) | ||||
| 
 | ||||
|     raw_input("\n\npress <enter> to go back home.\n\n") | ||||
|     redraw() | ||||
| 
 | ||||
|     return | ||||
| 
 | ||||
| def view_own(): | ||||
| 
 | ||||
|     filenames = [] | ||||
| 
 | ||||
|     for entry in os.listdir(DATA): | ||||
|         filenames.append(os.path.join(DATA, entry)) | ||||
|     metas = core.meta(filenames) | ||||
| 
 | ||||
|     entries = [] | ||||
|     for entry in metas: | ||||
|         entries.append(""+entry[4]+" ("+p.no("word", entry[2])+") ") | ||||
| 
 | ||||
|     return view_entries(metas, entries, "here are your recorded feels, listed by date: \n\n") | ||||
| 
 | ||||
| def show_credits(): | ||||
| 
 | ||||
|     print(""" | ||||
| ttbp was written by ~endorphant in python. the codebase is | ||||
| publicly available on github at https://github.com/modgethanc/ttbp | ||||
| 
 | ||||
| if you have ideas for ttbp, you are welcome to fork the repo and  | ||||
| work on it. i'm only a neophyte dev, so i apologize for any  | ||||
| horrendously ugly coding habits i have. i'd love to hear about your | ||||
| ideas and brainstorm about new features! | ||||
| 
 | ||||
| thanks to everyone who reads, listens, writes, and feels.\ | ||||
|         """) | ||||
| 
 | ||||
|     raw_input("\n\npress <enter> to go back home.\n\n") | ||||
|     redraw() | ||||
| 
 | ||||
|     return | ||||
| 
 | ||||
| 
 | ||||
| ## handlers | ||||
| 
 | ||||
| def write_entry(entry=os.path.join(DATA, "test.txt")): | ||||
| 
 | ||||
|     entered = raw_input("\nfeels will be recorded for today, "+time.strftime("%d %B %Y")+".\n\nif you've already started recording feels for this day, you \ncan pick up where you left off.\n\npress <enter> to begin recording your feels.\n\n") | ||||
|     if entered: | ||||
|         entryFile = open(entry, "a") | ||||
|         entryFile.write("\n"+entered+"\n") | ||||
|         entryFile.close() | ||||
|     subprocess.call([SETTINGS["editor"], entry]) | ||||
|     core.load_files() | ||||
|     core.write("index.html") | ||||
|     redraw("posted to "+LIVE+USER+"/"+SETTINGS["publish dir"]+"/index.html\n\nthanks for sharing your feels!") | ||||
|     return | ||||
| 
 | ||||
| def send_feedback(entered, subject="none", mailbox=os.path.join(FEEDBACK, USER+"-"+time.strftime("%Y%m%d-%H%M")+".msg")): | ||||
| 
 | ||||
|     message = "" | ||||
| 
 | ||||
|     temp = tempfile.NamedTemporaryFile() | ||||
|     if entered: | ||||
|         msgFile = open(temp.name, "a") | ||||
|         msgFile.write(entered+"\n") | ||||
|         msgFile.close() | ||||
|     subprocess.call([SETTINGS["editor"], temp.name]) | ||||
|     message = open(temp.name, 'r').read() | ||||
| 
 | ||||
|     id = "#"+util.genID(3) | ||||
|     mail = MIMEText(message) | ||||
|     mail['To'] = FEEDBOX | ||||
|     mail['From'] = USER+"@tilde.town" | ||||
|     mail['Subject'] = " ".join(["[ttbp]", subject, id]) | ||||
|     m = os.popen("/usr/sbin/sendmail -t -oi", 'w') | ||||
|     m.write(mail.as_string()) | ||||
|     m.close() | ||||
| 
 | ||||
|     return """\ | ||||
| thanks for writing! for your reference, it's been recorded | ||||
| > as "+ " ".join([subject, id])+". i'll try to respond to you soon.\ | ||||
|             """ | ||||
| 
 | ||||
| def view_entries(metas, entries, prompt): | ||||
| 
 | ||||
|     print_menu(entries) | ||||
| 
 | ||||
|     choice = list_select(entries, "pick an entry from the list, or type 'back' to go home: ") | ||||
| 
 | ||||
|     if choice is not False: | ||||
| 
 | ||||
|         redraw("now reading ~"+metas[choice][5]+"'s feels on "+metas[choice][4]+"\n> press <q> to return to feels list.\n\n") | ||||
| 
 | ||||
|         show_entry(metas[choice][0]) | ||||
|         redraw(prompt) | ||||
| 
 | ||||
|         return view_entries(metas, entries, prompt) | ||||
| 
 | ||||
|     else: | ||||
|         redraw() | ||||
|         return | ||||
| 
 | ||||
| def show_entry(filename): | ||||
| 
 | ||||
|     subprocess.call(["less", filename]) | ||||
| 
 | ||||
|     return | ||||
| 
 | ||||
| def view_feed(): | ||||
| 
 | ||||
|     feedList = [] | ||||
| 
 | ||||
|     for townie in find_ttbps(): | ||||
|         entryDir = os.path.join("/home", townie, ".ttbp", "entries") | ||||
|         filenames = os.listdir(entryDir) | ||||
|         for entry in filenames: | ||||
|             ### REALLY MAKE A REAL FILENAME VALIDATOR | ||||
|             #fileSplit = os.path.splitext(entry) | ||||
|             #if len(fileSplit[0]) == 8 and fileSplit[1] == ".txt": | ||||
|             if core.valid(entry): | ||||
|                 feedList.append(os.path.join(entryDir, entry)) | ||||
| 
 | ||||
|     metas = core.meta(feedList) | ||||
|     metas.sort(key = lambda entry:entry[3]) | ||||
|     metas.reverse() | ||||
| 
 | ||||
|     entries = [] | ||||
|     for entry in metas[0:10]: | ||||
|         pad = "" | ||||
|         if len(entry[5]) < 8: | ||||
|             pad = "\t" | ||||
| 
 | ||||
|         entries.append("~"+entry[5]+pad+"\ton "+entry[3]+" ("+p.no("word", entry[2])+") ") | ||||
| 
 | ||||
|     #print_menu(entries) | ||||
|     view_entries(metas, entries, "most recent ten entries: \n\n") | ||||
| 
 | ||||
|     redraw() | ||||
| 
 | ||||
|     return | ||||
| ##### | ||||
| 
 | ||||
| def find_ttbps(): | ||||
|     # looks for users with a valid ttbp config and returns a list of them | ||||
|     users = [] | ||||
| 
 | ||||
|     for townie in os.listdir("/home"): | ||||
|         if os.path.exists(os.path.join("/home", townie, ".ttbp", "config", "ttbprc")): | ||||
|             users.append(townie) | ||||
| 
 | ||||
|     return users | ||||
| 
 | ||||
| def list_select(options, prompt): | ||||
|     # runs the prompt for the list until a valid index is imputted | ||||
| 
 | ||||
|     ans = "" | ||||
|     invalid = True | ||||
| 
 | ||||
|     while invalid: | ||||
|         #try: | ||||
|         #    choice = raw_input("\n\n"+prompt) | ||||
|         #except KeyboardInterrupt: | ||||
|         #    redraw() | ||||
|         #    main_menu() | ||||
| 
 | ||||
|         choice = raw_input("\n\n"+prompt) | ||||
|         if choice in BACKS: | ||||
|             return False | ||||
| 
 | ||||
|         try: | ||||
|             ans = int(choice) | ||||
|         except ValueError: | ||||
|             return list_select(options, prompt) | ||||
| 
 | ||||
|         invalid = False | ||||
| 
 | ||||
|     if ans >= len(options): | ||||
|         return list_select(options, prompt) | ||||
| 
 | ||||
|     return ans | ||||
| 
 | ||||
| ##### | ||||
| 
 | ||||
| start() | ||||
							
								
								
									
										12
									
								
								bin/util.py
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								bin/util.py
									
									
									
									
									
								
							| @ -2,6 +2,7 @@ | ||||
| 
 | ||||
| import inflect | ||||
| import time | ||||
| import random | ||||
| 
 | ||||
| p = inflect.engine() | ||||
| 
 | ||||
| @ -27,3 +28,14 @@ def pretty_time(time): | ||||
|             return p.no("minute", m) | ||||
|     else: | ||||
|         return p.no("second", s) | ||||
| 
 | ||||
| def genID(digits=5): | ||||
|     # makes a string of digits | ||||
| 
 | ||||
|     id = "" | ||||
|     x  = 0 | ||||
|     while x < digits: | ||||
|         id += str(random.randint(0,9)) | ||||
|         x += 1 | ||||
| 
 | ||||
|     return id | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user