plugin.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. #####################################################################
  2. # #
  3. # THIS IS A SOURCE CODE FILE FROM A PROGRAM TO INTERACT WITH THE #
  4. # LBRY PROTOCOL ( lbry.com ). IT WILL USE THE LBRY SDK ( lbrynet ) #
  5. # FROM THEIR REPOSITORY ( https://github.com/lbryio/lbry-sdk ) #
  6. # WHICH I GONNA PRESENT TO YOU AS A BINARY. SINCE I DID NOT DEVELOP #
  7. # IT AND I'M LAZY TO INTEGRATE IN A MORE SMART WAY. THE SOURCE CODE #
  8. # OF THE SDK IS AVAILABLE IN THE REPOSITORY MENTIONED ABOVE. #
  9. # #
  10. # ALL THE CODE IN THIS REPOSITORY INCLUDING THIS FILE IS #
  11. # (C) J.Y.Amihud and Other Contributors 2021. EXCEPT THE LBRY SDK. #
  12. # YOU CAN USE THIS FILE AND ANY OTHER FILE IN THIS REPOSITORY UNDER #
  13. # THE TERMS OF GNU GENERAL PUBLIC LICENSE VERSION 3 OR ANY LATER #
  14. # VERSION. TO FIND THE FULL TEXT OF THE LICENSE GO TO THE GNU.ORG #
  15. # WEBSITE AT ( https://www.gnu.org/licenses/gpl-3.0.html ). #
  16. # #
  17. # THE LBRY SDK IS UNFORTUNATELY UNDER THE MIT LICENSE. IF YOU ARE #
  18. # NOT INTENDING TO USE MY CODE AND JUST THE SDK. YOU CAN FIND IT ON #
  19. # THEIR OFFICIAL REPOSITORY ABOVE. THEIR LICENSE CHOICE DOES NOT #
  20. # SPREAD ONTO THIS PROJECT. DON'T GET A FALSE ASSUMPTION THAT SINCE #
  21. # THEY USE A PUSH-OVER LICENSE, I GONNA DO THE SAME. I'M NOT. #
  22. # #
  23. # THE LICENSE CHOSEN FOR THIS PROJECT WILL PROTECT THE 4 ESSENTIAL #
  24. # FREEDOMS OF THE USER FURTHER, BY NOT ALLOWING ANY WHO TO CHANGE #
  25. # THE LICENSE AT WILL. SO NO PROPRIETARY SOFTWARE DEVELOPER COULD #
  26. # TAKE THIS CODE AND MAKE THEIR USER-SUBJUGATING SOFTWARE FROM IT. #
  27. # #
  28. #####################################################################
  29. # This file will be dealing with handling extensions aka plugins.
  30. import inspect
  31. import hashlib
  32. import json
  33. import sys
  34. import os
  35. import re
  36. import urllib.request
  37. from flbry import settings
  38. from flbry.variables import *
  39. from flbry import markdown
  40. from flbry import publish
  41. from subprocess import *
  42. def get_pluginignore():
  43. """Returns a list containing the parsed pluginignore file from the FastLBRY settings directory
  44. The pluginignore file is a list of regex strings for plugins that should be ignored.
  45. Lines beginning with a '#' are comments. To start a regex string with a '#', use '\#' instead.
  46. """
  47. # Read the pluginignore file if it exists
  48. plugin_ignore = settings.get_settings_folder() + "pluginignore"
  49. if os.path.exists(plugin_ignore):
  50. with open(plugin_ignore) as f:
  51. plig = f.readlines()
  52. # Remove comments and replace escaped number signs with unescaped ones
  53. plugin_ignore = []
  54. for expression in plig:
  55. expression = expression.strip()
  56. if not expression.startswith("#"):
  57. if expression.startswith("\#"):
  58. expression = expression.replace("\#", "#", 1)
  59. plugin_ignore.append(expression)
  60. else:
  61. plugin_ignore = []
  62. return plugin_ignore
  63. def ignore_plugin(plugin, plugin_ignore):
  64. """Return whether a plugin is in a list of regular expressions"""
  65. for expression in plugin_ignore:
  66. if re.fullmatch(expression, plugin):
  67. return True
  68. return False
  69. def run(args=[], command=False, address=None, execute=True, run_only=None):
  70. # This function will pass arguments to the plugin function.
  71. # The plugin will edit those arguments and return them back.
  72. # This is when we return those arguments back to the function
  73. # from where the plugin.run() was called.
  74. # To make it so anybody could extend any part of the software
  75. # without adding new paths to things inside the software code,
  76. # we are going to use an address of the function.
  77. # So for example from 'flbry' you've imported 'markdown' and
  78. # now it runs the Open() function. If inside that function we
  79. # will add plugin.run(), the resulting function address will be
  80. # 'flbry.markdwon.Open'
  81. # This is how we get the address:
  82. if not address:
  83. address = inspect.stack()[1].filename
  84. address = address.replace(os.getcwd(), "").replace(".py", "")
  85. address = address.replace("/",".")[1:]
  86. address = address +"."+ inspect.stack()[1].function
  87. # Now we want to get files from the plugins folder.
  88. plugins_folder = settings.get_settings_folder("flbry/plugins/")
  89. sys.path.append(settings.get_settings_folder())
  90. # Before we import we need to make sure that __init__.py exists
  91. # this is an empty file that will tell python that this folder is
  92. # indeed a module.
  93. if not os.path.exists(plugins_folder+"__init__.py"):
  94. t = open(plugins_folder+"__init__.py", "w")
  95. t.close()
  96. plugin_ignore = get_pluginignore()
  97. # Now let's import the files from plugins
  98. for p in os.listdir(plugins_folder):
  99. m = p[:-3]
  100. # If only a certain plugin is to run
  101. if run_only and m != run_only:
  102. continue
  103. # Ignore the __init__.py
  104. if m == "__init__":
  105. continue
  106. if ignore_plugin(m, plugin_ignore):
  107. continue
  108. if p.endswith(".py") and enabled(m):
  109. exec("from plugins import "+m)
  110. # Let's get the plugin_data of the plugin
  111. try:
  112. plugin_data = eval(m+".plugin_data")
  113. except:
  114. continue
  115. # Now let's update the data of the plugin_data
  116. # into the info of the plugin settings.
  117. plugin_info = {}
  118. for i in plugin_data:
  119. if i != "functions":
  120. plugin_info[i] = plugin_data[i]
  121. enabled(m, plugin_info=plugin_info)
  122. # Now that we have it let's compare it to the
  123. # address of the function from which it's running
  124. for func in plugin_data["functions"]:
  125. if address in func and not command and not func["command"] and execute:
  126. args = func[address](args)
  127. elif address in func and command and command.startswith(func["command"]) and execute:
  128. args = func[address](command, args)
  129. # Adding functions into completer
  130. if address in func and func["command"]:
  131. complete([func["command"]], add=True)
  132. # If we run only one plugin and the plugin is disabled.
  133. elif run_only and run_only == m:
  134. center("Plugin '"+m+"' is disabled!", "bdrd")
  135. return args
  136. def check_settings_exists():
  137. """Ensures that ~/.local/share/flbry/plugins.json exits"""
  138. if not os.path.exists(settings.get_settings_folder()+"plugins.json"):
  139. with open(settings.get_settings_folder()+"plugins.json", 'w') as f:
  140. json.dump({}, f, indent=4, sort_keys=True)
  141. def enabled(plugin_name, full_report=False, flip=False, plugin_info={}):
  142. """Checks whether a plugin is enabled in plugin settings."""
  143. # Firs let's make sure that the file exist
  144. check_settings_exists()
  145. # Then let's open the file
  146. with open(settings.get_settings_folder()+"plugins.json") as f:
  147. data = json.load(f)
  148. # Adding a missing plugin
  149. default = {"active":False,
  150. "info":{"title":plugin_name,
  151. "author":None,
  152. "license":None,
  153. "flbry":"terminal",
  154. "description":None}}
  155. # Ignore plugins
  156. plugin_ignore = get_pluginignore()
  157. if ignore_plugin(plugin_name, plugin_ignore):
  158. return False
  159. if plugin_name not in data:
  160. data[plugin_name] = default
  161. # Overwriting
  162. if flip:
  163. data[plugin_name]["active"] = not data[plugin_name]["active"]
  164. # Updating info
  165. if plugin_info:
  166. data[plugin_name]["info"] = plugin_info
  167. # Saving plugins file
  168. with open(settings.get_settings_folder()+"plugins.json", 'w') as f:
  169. json.dump(data, f, indent=4, sort_keys=True)
  170. # Returning data
  171. if full_report:
  172. return data[plugin_name]
  173. else:
  174. return data[plugin_name]["active"]
  175. def manager(search=""):
  176. """Gives a settings prompt to set various settings on plugins."""
  177. to_text = True
  178. plugins_commands = [
  179. "help",
  180. "read",
  181. "set",
  182. "publish",
  183. "description"
  184. ]
  185. while True:
  186. complete(plugins_commands)
  187. print()
  188. check_settings_exists()
  189. with open(settings.get_settings_folder()+"plugins.json") as f:
  190. data = json.load(f)
  191. data = run(data) # Updating the plugins
  192. d = {"categories":["Active", "Title", "Author", "License"],
  193. "size":[1, 3, 2, 2],
  194. "data":[]}
  195. plugin_ignore = get_pluginignore()
  196. for plugin in data:
  197. # Don't show ignored plugins
  198. if ignore_plugin(plugin, plugin_ignore):
  199. continue
  200. # Make sure you can get the nessesary data
  201. # even if it's somewhat corrupted.
  202. active = "[ ]"
  203. title = plugin
  204. description = ""
  205. LICENSE = ""
  206. author = ""
  207. try:
  208. active = data[plugin]["active"]
  209. except:
  210. pass
  211. try:
  212. if data[plugin]["info"]["title"]:
  213. title = data[plugin]["info"]["title"]
  214. except:
  215. pass
  216. try:
  217. if data[plugin]["info"]["description"]:
  218. description = data[plugin]["info"]["description"]
  219. except:
  220. pass
  221. try:
  222. if data[plugin]["info"]["license"]:
  223. LICENSE = data[plugin]["info"]["license"]
  224. except:
  225. pass
  226. try:
  227. if data[plugin]["info"]["author"]:
  228. author = data[plugin]["info"]["author"]
  229. except:
  230. pass
  231. # Make so the search works.
  232. if search and \
  233. search.lower() not in title.lower() \
  234. and search.lower() not in author.lower() \
  235. and search.lower() not in description.lower() \
  236. and search.lower() not in LICENSE.lower():
  237. continue
  238. # Let's add the plugin into the data
  239. d["data"].append([active, title, author, LICENSE])
  240. table(d)
  241. center("")
  242. # Now let's start the madness
  243. print()
  244. c = input(typing_dots("Type 'help' for more info.", to_text))
  245. to_text = False
  246. if not c:
  247. break
  248. try:
  249. c = int(c)
  250. enabled(list(data.keys())[c], flip=True)
  251. continue
  252. except:
  253. pass
  254. if c.startswith("description"):
  255. cn = get_cn(c, "Which plugin?")
  256. try:
  257. description = list(data.values())[cn]["info"]["description"]
  258. savedes = open("/tmp/fastlbrylastdescription.md", "w")
  259. savedes.write(description)
  260. savedes.close()
  261. markdown.draw("/tmp/fastlbrylastdescription.md", "Description")
  262. except:
  263. center("This plugin has no description.")
  264. elif c.startswith("read"):
  265. cn = get_cn(c, "Which plugin?")
  266. try:
  267. plugin_name = list(data.keys())[cn]
  268. plugin_file = settings.get_settings_folder("flbry/plugins/")+plugin_name+".py"
  269. markdown.draw(plugin_file, plugin_name+" Source Code", False)
  270. except:
  271. center("Plugin is deleted or corrupted", "bdrd")
  272. elif c.startswith("set"):
  273. cn = get_cn(c, "Which plugin?")
  274. run([None], address="settings", run_only=list(data.keys())[cn])
  275. elif c.startswith("publish"):
  276. cn = get_cn(c, "Which plugin?")
  277. try:
  278. plugin_name = list(data.keys())[cn]
  279. plugin_file = settings.get_settings_folder("flbry/plugins/")+plugin_name+".py"
  280. publish_plugin(plugin_file, list(data.values())[cn]["info"])
  281. except:
  282. center("Plugin is deleted or corrupted", "bdrd")
  283. elif c == "help":
  284. markdown.draw("help/plugins.md", "Plugins Help")
  285. def publish_plugin(filename, info):
  286. """Helps the user publish a plugin"""
  287. print()
  288. # Check if the plugin has a license in the info and get the name and link to it
  289. # Getting the link only works if the license it a SPDX Lincense Identifier: https://spdx.org/licenses/
  290. if "license" in info:
  291. l = spdx_license(info["license"])
  292. if "link" in l:
  293. info["license_url"] = l["link"]
  294. info["license_name"] = l["name"]
  295. info.pop("license")
  296. info["file"] = filename
  297. if not "version" in info:
  298. info["version"] = 1.0
  299. if not "fee" in info:
  300. info["fee"] = 0
  301. to_text = True
  302. publish_commands = [
  303. "help",
  304. "file",
  305. "link",
  306. "title",
  307. "author",
  308. "license",
  309. "description",
  310. "publish",
  311. "fastlbry",
  312. "version",
  313. "fee",
  314. "publish_file"
  315. ]
  316. editor = settings.get("default_editor")
  317. while True:
  318. complete(publish_commands)
  319. if "license_name" in info:
  320. d_license = info["license_name"]
  321. elif "license" in info:
  322. d_license = info["license"]
  323. else:
  324. d_license = "[no license]"
  325. d = {"categories":[ "Title", "Version", "Fee", "Author", "License"],
  326. "size":[ 3, 1, 1, 2, 2],
  327. "data":[[
  328. info["title"],
  329. info["version"],
  330. str(info["fee"]),
  331. info["author"],
  332. d_license
  333. ]]}
  334. table(d, False)
  335. if not "description" in info:
  336. info["description"] = ""
  337. d = {"categories": ["Description"],
  338. "size": [1],
  339. "data": [[info["description"]]]}
  340. table(d, False)
  341. d = {"categories": ["File or Link", "FastLBRY Variant"],
  342. "size": [3, 1],
  343. "data": [[info["file"], info["flbry"]]]}
  344. table(d, False)
  345. center("")
  346. c = input(typing_dots("Type 'help' for more info.", to_text, give_space=True))
  347. to_text = False
  348. if not c:
  349. break
  350. elif c == "publish_file":
  351. try:
  352. info["file"] = publish.configure(info["file"])['outputs'][0]['permanent_url']
  353. except Exception as e:
  354. center("Error: "+str(e), "bdrd")
  355. elif c.startswith(("file", "link")):
  356. if " " in c:
  357. info["file"] = c[c.find(" ")+1:]
  358. else:
  359. info["file"] = input(typing_dots("File or URL", give_space=True, to_add_dots=True))
  360. elif c.startswith("title"):
  361. if " " in c:
  362. info["title"] = c[c.find(" ")+1:]
  363. else:
  364. info["title"] = input(typing_dots("Plugin title", give_space=True, to_add_dots=True))
  365. elif c.startswith("author"):
  366. if " " in c:
  367. info["author"] = c[c.find(" ")+1:]
  368. else:
  369. info["author"] = input(typing_dots("Author", give_space=True, to_add_dots=True))
  370. elif c == "license":
  371. info.pop("license", None)
  372. info["license_name"], info["license_url"] = choose_license()
  373. elif c.startswith("description"):
  374. description = "Type the description here. Don't forget to save. Then return to FastLBRY."
  375. c = c + ' '
  376. a = c[c.find(" "):]
  377. if len(a) > 1:
  378. info["description"] = file_or_editor(a, description)
  379. else:
  380. if editor:
  381. description = file_or_editor(args, desciption, editor)
  382. else:
  383. info["description"] = input(typing_dots("Description", give_space=True, to_add_dots=True))
  384. elif c.startswith("version"):
  385. if " " in c:
  386. info["version"] = c[c.find(" ")+1:]
  387. else:
  388. info["version"] = input(typing_dots("Version number", give_space=True, to_add_dots=True))
  389. elif c.startswith("fee"):
  390. if " " in c:
  391. info["fee"] = c[c.find(" ")+1:]
  392. else:
  393. info["fee"] = input(typing_dots("Fee", give_space=True, to_add_dots=True))
  394. elif c.startswith("fastlbry"):
  395. if " " in c:
  396. info["flbry"] = c[c.find(" ")+1:]
  397. else:
  398. complete(["terminal", "gtk", "all"])
  399. info["flbry"] = input(typing_dots("FastLBRY variant", give_space=True, to_add_dots=True))
  400. elif c == "publish":
  401. try:
  402. x = publish_plugin_blob(info["file"], info)
  403. if x:
  404. return
  405. except Exception as e:
  406. center("Error publishing plugin: "+str(e), "bdrd")
  407. elif c == "help":
  408. markdown.draw("help/publish_plugins.md", "Plugin Publishing Help")
  409. def publish_plugin_blob(filename, info):
  410. """Creates and publishes blobs for FastLBRY plugins.
  411. Arguments:
  412. filename -- the filename of the work to be published. This can be an HTTP(S) or LBRY URL to the file.
  413. info -- a dictionary of info about the file including:
  414. file -- the filename of the work. Equivilent to filename.
  415. title -- the title of the work.
  416. license_name -- the name of the license the work can be copied under.
  417. license_url (optional) -- the URL of the license the work can be copied under.
  418. description (optional) -- a description of the work.
  419. fee (optional) -- the fee that will be charged for the work. This is ignored if the filename is a URL.
  420. """
  421. filename = os.path.expanduser(filename)
  422. # Just in case it does not have a fee
  423. if not "fee" in info:
  424. info["fee"] = 0
  425. # Try to open the file and if that fails treat it as a URL
  426. try:
  427. pf = open(filename, "rb")
  428. except FileNotFoundError:
  429. if filename.startswith("lbry://"):
  430. filename = filename.replace("lbry://", "https://spee.ch/")
  431. try:
  432. pf = urllib.request.urlopen(filename)
  433. if not info["fee"] in [0, "0"]:
  434. center("Plugin file is a link, fee will be ignored", "bdrd")
  435. except urllib.error.URLError:
  436. center("File is not a valid file or URL", "bdrd")
  437. return
  438. # We need a hash of the file for security reasons
  439. pf = pf.read()
  440. sha512 = hashlib.sha512(pf).hexdigest()
  441. info["sha512"] = sha512
  442. # Try to upload the plugin to LBRY
  443. info["file"] = publish.speech_upload(info["file"], name=sha512, fee=info["fee"], speech=False)
  444. # Saving it to json for publishing
  445. with open('/tmp/flbrypublishpluginblob.json', 'w') as f:
  446. json.dump(info, f, indent=4, sort_keys=True)
  447. data = {"name":info["title"],
  448. "bid":0.001,
  449. "file_path":'/tmp/flbrypublishpluginblob.json',
  450. "title":info["title"],
  451. "license":info["license_name"],
  452. "thumbnail_url":"",
  453. "channel_id":"",
  454. "channel_name":"",
  455. "fee_amount":0,
  456. "tags":["FastLBRY-terminal-plugin-blob-json-file"]
  457. }
  458. if "description" in info:
  459. data["description"] = info["description"]
  460. if "license_url" in info:
  461. data["license_url"] = info["license_url"],
  462. publish.configure('/tmp/flbrypublishpluginblob.json', data)
  463. return True
  464. def get_plugin(search=""):
  465. """Searches and installs plugins."""
  466. # ^ I hate this syntax, but alright. :|
  467. w, h = tsize()
  468. page_size = h - 5
  469. page = 1
  470. while True:
  471. # Since it's plugins and we will need to read each of their
  472. # blob data file. It's gonna require a ratehr more combersome
  473. # way of dealing with it.
  474. # We will need to show a progress bar each time
  475. print()
  476. progress_bar(0, page_size, "Searching plugins...")
  477. args = [lbrynet_binary["b"], "claim", "search",
  478. "--any_tags=FastLBRY-terminal-plugin-blob-json-file", # Block tag.
  479. "--fee_amount=0", # Blobs should be gratis, since we will download them asap
  480. '--page='+str(page),
  481. '--page_size='+str(page_size),
  482. "--no_totals",
  483. '--order_by=release_time']
  484. if search:
  485. args.append('--text="'+search+'"')
  486. out = check_output(args)
  487. try:
  488. out = json.loads(out)["items"]
  489. except:
  490. center("Connect to LBRY first.", "bdrd")
  491. return
  492. # We gonna download the blob for each plugin and read it
  493. # right away.
  494. data_print = {"categories":["Author", "Plugin Name", "Version", "Price", "License"],
  495. "size":[1,4,1,1,4],
  496. "data":[]}
  497. for n, plugin in enumerate(out):
  498. name = plugin["name"]
  499. try:
  500. name = plugin["value"]["title"]
  501. except:
  502. pass
  503. progress_bar(n+1, len(out), "Loading "+name+"...")
  504. try:
  505. blob = check_output([lbrynet_binary["b"], "get", plugin["permanent_url"],
  506. "--file_name=flbrypluginblob.json", # How to call
  507. "--download_directory=/tmp"]) # Where to save
  508. with open("/tmp/flbrypluginblob.json") as f:
  509. blob = json.load(f)
  510. except:
  511. continue
  512. # Now that we have the blob, let's show the data
  513. author = "[no author]"
  514. try:
  515. author = blob["author"]
  516. except:
  517. pass
  518. title = "[no title]"
  519. try:
  520. title = blob["title"]
  521. except:
  522. pass
  523. version = 0.0
  524. try:
  525. version = blob["version"]
  526. except:
  527. pass
  528. fee = "Gratis"
  529. try:
  530. # The problem is, the fee should be of a file, not of
  531. # what ever is in the blob.
  532. fee = blob["fee"]
  533. except:
  534. pass
  535. license_is = False
  536. try:
  537. license_is = blob["license_name"]
  538. except:
  539. pass
  540. data_print["data"].append([author, title, version, fee, license_is])
  541. # TODO: Make it work. I'm tired and I don't understand what to come next.
  542. print()
  543. table(data_print)
  544. center("")
  545. return