run.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  1. # This is a little script that will generate promotional parts for
  2. # your articles on LBRY network. Using Markdown.
  3. # This script is under the terms of the GNU General Public License
  4. # version 3 or any later version. (c) J.Y.Amihud 2021.
  5. ############# DEPENDENCIES #############
  6. # This is not a software project intended for any-who. It's interned
  7. # for the advanced user. And thus I will not provide a script to
  8. # install dependecies. Mainly because I'm lazy. You can pull-request
  9. # me one.
  10. # This will need:
  11. # lbrynet executable. That you can either on:
  12. # https://github.com/lbryio/lbry-sdk/releases
  13. # or in the FastLBRY project:
  14. # https://notabug.org/jyamihud/FastLBRY-terminal
  15. # The creation of images will be done using:
  16. # cairo
  17. #
  18. # And also the following imports should all be present on your system's
  19. # python installation.
  20. ################# CODE ##################
  21. import os
  22. import sys # 'python3 run.py -t' will activate test mode
  23. import json
  24. import math
  25. import cairo
  26. import random
  27. import urllib.request
  28. import urllib3
  29. import subprocess
  30. from PIL import Image
  31. def comment_request(method: str, params: dict, addition=""):
  32. # Part of FastLBRY project
  33. """
  34. Sends a request to the comment API
  35. """
  36. data = {
  37. "method": method,
  38. "id": 1,
  39. "jsonrpc":"2.0",
  40. "params": params
  41. }
  42. data = json.dumps(data).encode()
  43. headers = {
  44. "Content-Type": "application/json"
  45. }
  46. try:
  47. req = urllib.request.Request(comments_api, data, headers)
  48. res = urllib.request.urlopen(req)
  49. out = res.read().decode()
  50. return json.loads(out)
  51. except Exception as e:
  52. print("COMMENT ERROR:", e)
  53. return
  54. # The LBRY SDK reads your account data from ~/.config/lbry folder
  55. # which is shared with LBRY Desktop and other LBRY based apps. So
  56. # you will need to login from it.
  57. # Also, the SDK should be already started. The script is
  58. # dumb and will not check for it. You will need to use
  59. # lbrynet start
  60. # Before running this script.
  61. # Here put the full file_path of the SDK on your system.
  62. SDK = "/home/vcs/Software/FastLBRY-GTK/flbry/lbrynet"
  63. comments_api = "https://comments.odysee.com/api/v2"
  64. # Let's get the latest publication's claim_id so we could fetch it's
  65. # comments.
  66. # HOW MANY PUBLICATIONS TO FETCH?
  67. DATA_C = [] # This data will be populated with comments Data, so later
  68. # I could draw an image from it.
  69. visited = [] # In case you've edited a publication.
  70. amount = ""
  71. while True:
  72. amount = input("HOW MANY RECENT PUBLICATIONS TO LOAD? : ")
  73. try:
  74. amount = int(amount)
  75. break
  76. except:
  77. print("AMOUNT SHOULD BE A NUMBER!")
  78. lbc = ""
  79. while True:
  80. lbc = input("THE SUPPORT SHOULD BE ABOVE? : ")
  81. try:
  82. lbc = int(lbc)
  83. break
  84. except:
  85. print("IT SHOULD BE A NUMBER!")
  86. fetch = ""
  87. while True:
  88. fetch = input("HOW MANY COMMENTS TO FETCH FROM A PUBLICATION? : ")
  89. try:
  90. fetch = int(fetch)
  91. break
  92. except:
  93. print("IT SHOULD BE A NUMBER!")
  94. print("FETCHING THE CLAIM_ID FOR THE LAST "+str(amount)+" PUBLICATIONS ...")
  95. out = subprocess.check_output([SDK, "stream", "list",
  96. '--page=1', # FIRST PUBLICATION
  97. '--page_size='+str(amount), # LOAD SELECTED AMOUNT
  98. "--no_totals"])
  99. # Now let's convert what ever we received into a good data
  100. print("READING THE DATA RECEIVED ...")
  101. l = json.loads(out)
  102. for out in l["items"]:
  103. if out["permanent_url"] in visited:
  104. continue
  105. else:
  106. visited.append(out["permanent_url"])
  107. # Let's give a user a little soothing information about what was
  108. # actually received.
  109. try:
  110. print("PUBLICATION: "+out["value"]["title"])
  111. except:
  112. print("PUBLICATION: "+out["permanent_url"])
  113. # Fetching the transactions of the publication
  114. print("FETCHING COMMNET-LESS SUPPORT ...")
  115. txo = subprocess.check_output([SDK,
  116. "txo", "list", '--claim_id='+out["claim_id"],
  117. '--page=1',
  118. '--page_size='+str(fetch) # AMOUNT OF TXO TO LOAD
  119. ])
  120. txo = json.loads(txo)
  121. # This is a bit hacky, but we all love hacking so...
  122. # basically in each Tip for a publication you can see
  123. # a channel's claim_id.
  124. # Often it's 7d0b0f83a195fd1278e1944ddda4cda8d9c01a56
  125. # but this claim_id belongs to LBRY Fund channel that
  126. # does tipping based on views.
  127. # We want to see anything but this channel.
  128. for t in txo["items"]:
  129. if "signing_channel" in t and float(t["amount"]) > lbc:
  130. try:
  131. if t["signing_channel"]["channel_id"] != "7d0b0f83a195fd1278e1944ddda4cda8d9c01a56":
  132. # This means we found a support
  133. print("FOUND SUPPORT OF "+str(t["amount"]))
  134. # I found that LBRY doesn't give a damn about the name
  135. # if you have the claim_id.
  136. print("FETCHING DATA ABOUT SENDER ...")
  137. sender = subprocess.check_output([SDK,
  138. "claim","search", "--claim_id="+t["signing_channel"]["channel_id"]])
  139. sender = json.loads(sender)
  140. sender = sender["items"][0]
  141. #print(t)
  142. #sender = sender["a#"+t["signing_channel"]["channel_id"]]
  143. icon = ""
  144. banner = ""
  145. title = ""
  146. try:
  147. title = sender["name"]
  148. except:
  149. pass
  150. try:
  151. icon = sender["value"]["thumbnail"]["url"]
  152. except:
  153. pass
  154. try:
  155. banner = sender["value"]["cover"]["url"]
  156. except:
  157. pass
  158. try:
  159. title = sender["value"]["title"]
  160. except:
  161. pass
  162. try:
  163. link = sender["canonical_url"].replace("#",":").replace("lbry://", "")
  164. except:
  165. link = ""
  166. url_of_comment = out["permanent_url"].replace("#",":").replace("lbry://", "https://odysee.com/")
  167. print("SENDER NAME: "+title)
  168. print("SENDER BANNER: "+banner)
  169. print("SENDER ICON: "+icon)
  170. print("SENDER LINK: "+link)
  171. print("COMMENT LINK: "+url_of_comment)
  172. DATA_C.append([
  173. float(t["amount"]), # 0
  174. title, # 1
  175. "", # 2
  176. icon, # 3
  177. banner, # 4
  178. link, # 5
  179. url_of_comment # 6
  180. ])
  181. except:
  182. pass
  183. # Fetching comments from the publication
  184. print("FETCHING COMMENTS ...")
  185. # Old code, no longer supported by the new SDK
  186. # comments = subprocess.check_output([SDK,
  187. # "comment", "list", out["claim_id"],
  188. # '--page=1',
  189. # '--page_size='+str(fetch), # AMOUNT OF COMMENTS TO LOAD
  190. # '--include_replies'])
  191. params = {
  192. "claim_id": out["claim_id"],
  193. "page": 1,
  194. "page_size": fetch,
  195. "sort_by": 3,
  196. "top_level": False,
  197. }
  198. comments = comment_request("comment.List", params)["result"]
  199. # Trying to read the list
  200. # comments = json.loads(comments)
  201. print( str(comments["total_items"])+" COMMENTS FOUND" )
  202. if 'items' in comments:
  203. for c in comments["items"]:
  204. # Each comment will be checked for the amount
  205. if c["support_amount"] > lbc:
  206. # So we found a supporter comment
  207. try:
  208. print(c["channel_name"], "SUPPORT: "+str(c["support_amount"]),
  209. "["+c["comment"][:20]+"...]")
  210. except:
  211. print("ANONYMOUS", "SUPPORT: "+str(c["support_amount"]),
  212. "["+c["comment"][:20]+"...]")
  213. # We need to get the image of the channel.
  214. # and make a pretty picture for this comment.
  215. icon = ""
  216. banner = ""
  217. title = c["channel_name"]
  218. if "channel_id" in c:
  219. print("FETCHING DATA ABOUT SENDER ...")
  220. sender = subprocess.check_output([SDK,
  221. "resolve", c["channel_name"]+"#"+c["channel_id"]])
  222. sender = json.loads(sender)
  223. sender = sender[c["channel_name"]+"#"+c["channel_id"]]
  224. try:
  225. icon = sender["value"]["thumbnail"]["url"]
  226. except:
  227. pass
  228. try:
  229. banner = sender["value"]["cover"]["url"]
  230. except:
  231. pass
  232. try:
  233. title = sender["value"]["title"]
  234. except:
  235. pass
  236. try:
  237. link = sender["canonical_url"].replace("#",":").replace("lbry://", "")
  238. except:
  239. link = ""
  240. url_of_comment = out["permanent_url"].replace("#",":").replace("lbry://", "https://odysee.com/")\
  241. +"?lc="+c["comment_id"]
  242. print("SENDER NAME: "+title)
  243. print("SENDER BANNER: "+banner)
  244. print("SENDER ICON: "+icon)
  245. print("SENDER LINK: "+link)
  246. print("COMMENT LINK: "+url_of_comment)
  247. # Because we checked earlier for the support not related
  248. # to the comments. The supported comment ( if chose to filter
  249. # by more then 0 LBC ) will be already added but without the
  250. # comments string. So we need to delete it first.
  251. found = False
  252. for item in DATA_C:
  253. if c["support_amount"] in item\
  254. and title in item\
  255. and icon in item\
  256. and banner in item\
  257. and out["permanent_url"].replace("#",":").replace("lbry://", "https://odysee.com/") in item\
  258. and link in item:
  259. found = item
  260. break
  261. if found:
  262. DATA_C.remove(found)
  263. DATA_C.append([
  264. c["support_amount"], # 0
  265. title, # 1
  266. c["comment"], # 2
  267. icon, # 3
  268. banner, # 4
  269. link, # 5
  270. url_of_comment # 6
  271. ])
  272. # Now that we have all the comments ready in the DATA_C list
  273. # we can start drawing the comments.
  274. # The idea is to have a .md formatted text that you can add in
  275. # the end of the article. With images uploaded and ready to go.
  276. def roundrect(layer, x, y, width, height, r):
  277. # This function will trace a round rectangle
  278. if width < r*2:
  279. width = r*2
  280. if height < r*2:
  281. height = r*2
  282. layer.move_to(x,y+r)
  283. layer.arc(x+r, y+r, r, math.pi, 3*math.pi/2)
  284. layer.arc(x+width-r, y+r, r, 3*math.pi/2, 0)
  285. layer.arc(x+width-r, y+height-r, r, 0, math.pi/2)
  286. layer.arc(x+r, y+height-r, r, math.pi/2, math.pi)
  287. layer.close_path()
  288. def get_image(url, save_url):
  289. # This function will download an image and save it
  290. # as a PNG image
  291. filename = "images/"+str(save_url)
  292. # First let's download the image
  293. http = urllib3.PoolManager()
  294. r = http.request('GET', url, preload_content=False)
  295. with open(filename, 'wb') as out:
  296. while True:
  297. data = r.read(1024)
  298. if not data:
  299. break
  300. out.write(data)
  301. r.release_conn()
  302. # Then converting to png
  303. im = Image.open(filename)
  304. im.save(filename+".png")
  305. # REMOVING THE TEMPORARY ONE
  306. os.remove(filename)
  307. def fit(load2, width, height):
  308. # This will scale a layer to fit into an
  309. # area.
  310. newimage = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height)
  311. imagedraw = cairo.Context(newimage)
  312. factor = 1
  313. if (load2.get_height()*(width/load2.get_width()))\
  314. < height:
  315. factor = height / load2.get_height()
  316. else:
  317. factor = width / load2.get_width()
  318. #factor = 0.1
  319. imagedraw.scale(factor, factor)
  320. dx = (width/2)/factor -(load2.get_width() /2)
  321. dy = (height/2)/factor -(load2.get_height()/2)
  322. imagedraw.set_source_surface(load2, dx, dy)
  323. imagedraw.paint()
  324. return newimage
  325. MD = "# Supporters:\n\n"
  326. MD = MD + "*Showing hyper-comments and other support with more than "+str(lbc)+" LBC from the last "+str(amount)+" publications. "
  327. MD = MD + "Including publications by other channels of my account. It's done automatically using* [this](https://notabug.org/jyamihud/LBRY-Supported-Comments) *script.*\n\n"
  328. for n, c in enumerate(reversed(sorted(DATA_C))):
  329. print("DRAWING IMAGE FOR COMMENT NUMBER", n)
  330. image_width = 1100
  331. image_height = 200
  332. # Now let's make the cairo object
  333. surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, image_width, image_height)
  334. layer = cairo.Context(surface)
  335. layer.select_font_face("Dyuthi", cairo.FONT_SLANT_NORMAL, cairo.FONT_WEIGHT_NORMAL)
  336. # BACKGROUND
  337. roundrect(layer, 0, 0, image_width, image_height, 50)
  338. layer.set_source_rgba(0.21,0.22,0.29,1) # BACKGROUND IS #353849
  339. layer.fill()
  340. # AROUND THING
  341. roundrect(layer, 10, 10, image_width-20, image_height-20, 40)
  342. layer.set_line_width(20)
  343. layer.set_source_rgba(1,0.72,0,1) # YELLOW THING AROUND
  344. layer.stroke()
  345. # CLIP 1
  346. roundrect(layer, 20, 20, image_width-40, image_height-40, 30)
  347. layer.clip()
  348. # Banner
  349. try:
  350. if c[4]:
  351. get_image(c[4], str(n)+"banner")
  352. banner = cairo.ImageSurface.create_from_png("images/"+str(n)+"banner.png")
  353. banner = fit(banner, image_width-40, image_height-40)
  354. layer.set_source_surface(banner, 20, 20)
  355. layer.paint()
  356. # REMOVING THE BANNER ORIGNINAL
  357. os.remove("images/"+str(n)+"banner.png")
  358. except:
  359. pass
  360. # MAKING BANNER LESS VISIBLE
  361. roundrect(layer, 0, 0, image_width, image_height, 50)
  362. layer.set_source_rgba(0.21,0.22,0.29,0.75)
  363. layer.fill()
  364. #NAME OF THE CHANNEL
  365. layer.set_source_rgba(1,0.72,0,1) # YELLOW THING AROUND
  366. layer.set_font_size(60)
  367. layer.move_to(210,80)
  368. if c[1]:
  369. layer.show_text(c[1])
  370. else:
  371. layer.set_font_size(30)
  372. layer.show_text("Help... For some reason I can't get the channel resolved. Line: 149")
  373. # TODO: See the message on top. It's something to do with
  374. # line 149. Please help. I can't understand the problem.
  375. # It used to work. But no longer works.
  376. c[1] = "[Channel Name]"
  377. #LBC AMOUNT
  378. lbc = cairo.ImageSurface.create_from_png("images/lbc.png")
  379. layer.set_source_surface(lbc, 210, 110)
  380. layer.paint()
  381. layer.set_source_rgba(1,1,1,1) # WHITE
  382. layer.set_font_size(55)
  383. layer.move_to(260,150)
  384. layer.show_text(str(c[0]))
  385. # COMMENTED WITH:
  386. if c[2]:
  387. layer.set_source_rgba(1,0.72,0,1) # YELLOW THING AROUND
  388. layer.set_font_size(30)
  389. layer.move_to(800,150)
  390. layer.show_text("Commented with:")
  391. # CLIP 2 ICON
  392. roundrect(layer, 25, 25, 150, 150, 25)
  393. layer.clip()
  394. # Icon
  395. if c[3]:
  396. try:
  397. get_image(c[3], str(n)+"icon")
  398. icon = cairo.ImageSurface.create_from_png("images/"+str(n)+"icon.png")
  399. icon = fit(icon, 150, 150)
  400. layer.set_source_surface(icon, 25, 25)
  401. layer.paint()
  402. # REMOVING THE BANNER ORIGNINAL
  403. os.remove("images/"+str(n)+"icon.png")
  404. except:
  405. print("Icon Failed!")
  406. # SAVE TO IMAGE
  407. print("SAVING IMAGE ...")
  408. try:
  409. os.mkdir("images")
  410. except:
  411. pass
  412. surface.write_to_png("images/"+str(n)+".png")
  413. print("SAVED AS: images/"+str(n)+".png")
  414. # Uploading the IMAGE to the LBRY network
  415. # We gonna need the images to be online
  416. if not "-t" in sys.argv:
  417. print("UPLOADING IMAGE: images/"+str(n)+".png TO LBRY ...")
  418. lbryname = ""
  419. good = "qwertyuiopasdfghjklzxcvbnm-_QWERTYUIOPASDFGHJKLZXCVBNM1234567890"
  420. for i in range(30):
  421. lbryname = lbryname + random.choice(good)
  422. img = subprocess.check_output([
  423. SDK,
  424. "publish",
  425. "--name="+lbryname,
  426. "--bid=0.001",
  427. "--file_path="+os.getcwd()+"/images/"+str(n)+".png"])
  428. img = json.loads(img)
  429. imageurl = img['outputs'][0]['permanent_url'].replace("lbry://","https://spee.ch/")
  430. print("PUBLISHED : "+imageurl)
  431. else:
  432. # If testing mode
  433. imageurl = "images/"+str(n)+".png"
  434. # Adding the comment to the MD file
  435. MD = MD + "[![]("+imageurl+")]("+c[6]+")\n"
  436. if c[2]: # If there are any comments
  437. for line in c[2].split("\n"):
  438. bad_site = False
  439. # This feature will implement a redirect
  440. # or warning if a sender wants to link to
  441. # a website which are known to be nasty.
  442. filt = {
  443. "youtube.com/":"yewtu.be/",
  444. "youtu.be/":"yewtu.be/",
  445. "twitter.com/":"nitter.net/",
  446. "reddit.com/":"teddit.net/"
  447. }
  448. for site in filt:
  449. if site in line:
  450. line = line.replace(site, filt[site])
  451. bad_site = True
  452. MD = MD + "> "+line+"\n"
  453. if bad_site:
  454. MD = MD + "\n\n*This line included links to unsafe and non-free internet dis-services. The link was automatically edited to load a Free / Private alternative front end.*\n"
  455. MD = MD + "\n\n*I want to show appreciation to ["+c[1]+"](https://odysee.com/"+c[5]+") for supporting me with "+str(c[0])+" LBC coins. "
  456. MD = MD + "You can send some of your LBC to ["+c[1]+"](https://odysee.com/"+c[5]+") or follow ["+c[1]+"](https://odysee.com/"+c[5]+") to return the favor.*"
  457. MD = MD + "\n\n\n__________\n"
  458. print("----------- THE MD IS -------------")
  459. print(MD)
  460. print("-------------THE END---------------")
  461. savemd = open("output.md", "w")
  462. savemd.write(MD)
  463. savemd.close()
  464. print("SAVED AS: output.md")
  465. # Show preview if it's a test.
  466. if "-t" in sys.argv:
  467. os.system("xdg-open output.md")