From 3999dbd4a154773970a8c348408aa1ec235f5217 Mon Sep 17 00:00:00 2001 From: Dylan Date: Sun, 2 Oct 2022 21:01:35 +0100 Subject: [PATCH] Poll/Votes support, closes #17 --- msgs.py | 35 ++++++++++++++++++++++++++++++- test_vx.py | 18 +++++++++++++++- twitfix.py | 61 ++++++++++++++++++++++++++++++++++++++---------------- 3 files changed, 94 insertions(+), 20 deletions(-) diff --git a/msgs.py b/msgs.py index 0711f10..782602f 100644 --- a/msgs.py +++ b/msgs.py @@ -1,3 +1,6 @@ +from asyncore import poll + + failedToScan="Failed to scan your link! This may be due to an incorrect link, private/suspended account, deleted tweet, or Twitter itself might be having issues (Check here: https://api.twitterstat.us/)" failedToScanExtra = "\n\nTwitter gave me this error: " tweetNotFound="Tweet not found." @@ -8,4 +11,34 @@ def genLikesDisplay(vnf): def genQrtDisplay(qrt): verifiedCheck = "☑️" if ('verified' in qrt and qrt['verified']) else "" - return ("\n─────────────\n ➤ QRT of " + qrt['handle'] + " (@" + qrt['screen_name'] + ")"+ verifiedCheck+":\n─────────────\n'" + qrt['desc'] + "'") \ No newline at end of file + return ("\n─────────────\n ➤ QRT of " + qrt['handle'] + " (@" + qrt['screen_name'] + ")"+ verifiedCheck+":\n─────────────\n'" + qrt['desc'] + "'") + +def genPollDisplay(poll): + pctSplit=10 + output="\n\n" + for choice in poll["choices"]: + output+=choice["text"]+"\n"+("█"*int(choice["percent"]/pctSplit)) +" "+str(choice["percent"])+"%\n" + return output + +def formatEmbedDesc(type,body,qrt,pollDisplay,likesDisplay): + # Trim the embed description to 248 characters, prioritizing poll and likes + output = "" + if pollDisplay==None: + pollDisplay="" + + if type=="" or type=="Video": + output = body+pollDisplay + elif qrt=={}: + output= body+pollDisplay+likesDisplay + else: + qrtDisplay = genQrtDisplay(qrt) + output= body + qrtDisplay + likesDisplay + if len(output)>248: + # find out how many characters we need to remove + diff = len(output)-248 + print("diff: "+str(diff)) + # remove the characters from body, add ellipsis + body = body[:-(diff+1)]+"…" + return formatEmbedDesc(type,body,qrt,pollDisplay,likesDisplay) + else: + return output \ No newline at end of file diff --git a/test_vx.py b/test_vx.py index 3417286..b57eae3 100644 --- a/test_vx.py +++ b/test_vx.py @@ -12,16 +12,23 @@ testTextTweet="https://twitter.com/jack/status/20" testVideoTweet="https://twitter.com/Twitter/status/1263145271946551300" testMediaTweet="https://twitter.com/Twitter/status/1118295916874739714" testMultiMediaTweet="https://twitter.com/Twitter/status/1293239745695211520" +testPollTweet="https://twitter.com/norm/status/651169346518056960" textVNF_compare = {'tweet': 'https://twitter.com/jack/status/20', 'url': '', 'description': 'just setting up my twttr', 'screen_name': 'jack', 'type': 'Text', 'images': ['', '', '', '', ''], 'time': 'Tue Mar 21 20:50:14 +0000 2006', 'qrt': {}, 'nsfw': False} videoVNF_compare={'tweet': 'https://twitter.com/Twitter/status/1263145271946551300', 'url': 'https://video.twimg.com/amplify_video/1263145212760805376/vid/1280x720/9jous8HM0_duxL0w.mp4?tag=13', 'description': 'Testing, testing...\n\nA new way to have a convo with exactly who you want. We’re starting with a small % globally, so keep your 👀 out to see it in action. https://t.co/pV53mvjAVT', 'thumbnail': 'http://pbs.twimg.com/media/EYeX7akWsAIP1_1.jpg', 'screen_name': 'Twitter', 'type': 'Video', 'images': ['', '', '', '', ''], 'time': 'Wed May 20 16:31:15 +0000 2020', 'qrt': {}, 'nsfw': False,'verified': True, 'size': {'width': 1920, 'height': 1080}} testMedia_compare={'tweet': 'https://twitter.com/Twitter/status/1118295916874739714', 'url': '', 'description': 'On profile pages, we used to only show someone’s replies, not the original Tweet 🙄 Now we’re showing both so you can follow the conversation more easily! https://t.co/LSBEZYFqmY', 'thumbnail': 'https://pbs.twimg.com/media/D4TS4xeX4AA02DI.jpg', 'screen_name': 'Twitter', 'type': 'Image', 'images': ['https://pbs.twimg.com/media/D4TS4xeX4AA02DI.jpg', '', '', '', '1'], 'time': 'Tue Apr 16 23:31:38 +0000 2019', 'qrt': {}, 'nsfw': False, 'size': {}} testMultiMedia_compare={'tweet': 'https://twitter.com/Twitter/status/1293239745695211520', 'url': '', 'description': 'We tested, you Tweeted, and now we’re rolling it out to everyone! https://t.co/w6Q3Q6DiKz', 'thumbnail': 'https://pbs.twimg.com/media/EfJ-C-JU0AAQL_C.jpg', 'screen_name': 'Twitter', 'type': 'Image', 'images': ['https://pbs.twimg.com/media/EfJ-C-JU0AAQL_C.jpg', 'https://pbs.twimg.com/media/EfJ-aHlU0AAU1kq.jpg', '', '', '2'], 'time': 'Tue Aug 11 17:35:57 +0000 2020', 'qrt': {}, 'nsfw': False, 'verified': True, 'size': {}} +testPoll_comparePoll={"name":"poll2choice_text_only","binding_values":{"choice1_label":{"type":"STRING","string_value":"Mean one thing"},"choice2_label":{"type":"STRING","string_value":"Mean multiple things"},"end_datetime_utc":{"type":"STRING","string_value":"2015-10-06T22:57:24Z"},"counts_are_final":{"type":"BOOLEAN","boolean_value":True},"choice2_count":{"type":"STRING","string_value":"33554"},"choice1_count":{"type":"STRING","string_value":"124875"},"last_updated_datetime_utc":{"type":"STRING","string_value":"2015-10-06T22:57:31Z"},"duration_minutes":{"type":"STRING","string_value":"1440"}}} +testPoll_comparePollVNF={'total_votes': 158429, 'choices': [{'text': 'Mean one thing', 'votes': 124875, 'percent': 78.8}, {'text': 'Mean multiple things', 'votes': 33554, 'percent': 21.2}]} + def compareDict(original,compare): for key in original: assert key in compare - assert compare[key]==original[key] + if type(compare[key]) is not dict: + assert compare[key]==original[key] + else: + compareDict(original[key],compare[key]) ## Tweet retrieve tests ## def test_textTweetExtract(): @@ -66,6 +73,11 @@ def test_multimediaTweetExtract(): assert video["media_url_https"]=="https://pbs.twimg.com/media/EfJ-aHlU0AAU1kq.jpg" assert video["type"]=="photo" +def test_pollTweetExtract(): + tweet = twExtract.extractStatus("https://twitter.com/norm/status/651169346518056960") + assert 'card' in tweet + compareDict(testPoll_comparePoll,tweet['card']) + ## VNF conversion test ## def test_textTweetVNF(): @@ -85,6 +97,10 @@ def test_multimediaTweetVNF(): vnf = twitfix.link_to_vnf_from_unofficial_api(testMultiMediaTweet) compareDict(testMultiMedia_compare,vnf) +def test_pollTweetVNF(): + vnf = twitfix.link_to_vnf_from_unofficial_api(testPollTweet) + compareDict(testPoll_comparePollVNF,vnf['poll']) + ## Test adding to cache ; cache should be empty ## def test_addToCache(): cache.clearCache() diff --git a/twitfix.py b/twitfix.py index dbed626..3394105 100644 --- a/twitfix.py +++ b/twitfix.py @@ -248,7 +248,7 @@ def embed_video(video_link, image=0): # Return Embed from any tweet link return message(msgs.failedToScan+msgs.failedToScanExtra+e) return message(msgs.failedToScan) -def tweetInfo(url, tweet="", desc="", thumb="", uploader="", screen_name="", pfp="", tweetType="", images="", hits=0, likes=0, rts=0, time="", qrt={}, nsfw=False,ttl=None,verified=False,size={}): # Return a dict of video info with default values +def tweetInfo(url, tweet="", desc="", thumb="", uploader="", screen_name="", pfp="", tweetType="", images="", hits=0, likes=0, rts=0, time="", qrt={}, nsfw=False,ttl=None,verified=False,size={},poll=None): # Return a dict of video info with default values if (ttl==None): ttl = getDefaultTTL() vnf = { @@ -269,8 +269,11 @@ def tweetInfo(url, tweet="", desc="", thumb="", uploader="", screen_name="", pfp "nsfw" : nsfw, "ttl" : ttl, "verified" : verified, - "size" : size + "size" : size, + "poll" : poll } + if (poll is None): + del vnf['poll'] return vnf def link_to_vnf_from_tweet_data(tweet,video_link): @@ -323,6 +326,11 @@ def link_to_vnf_from_tweet_data(tweet,video_link): for eurl in tweet['entities']['urls']: text = text.replace(eurl["url"],eurl["expanded_url"]) + if 'card' in tweet and tweet['card']['name'].startswith('poll'): + poll=getPollObject(tweet['card']) + else: + poll=None + vnf = tweetInfo( url, video_link, @@ -338,7 +346,8 @@ def link_to_vnf_from_tweet_data(tweet,video_link): images=imgs, nsfw=nsfw, verified=tweet['user']['verified'], - size=size + size=size, + poll=poll ) return vnf @@ -404,19 +413,14 @@ def embed(video_link, vnf, image): urlLink = urllib.parse.quote(video_link) likeDisplay = msgs.genLikesDisplay(vnf) - try: - if vnf['type'] == "": - desc = desc - elif vnf['type'] == "Video": - desc = desc - elif vnf['qrt'] == {}: # Check if this is a QRT and modify the description - desc = (desc + likeDisplay) - else: - qrtDisplay = msgs.genQrtDisplay(vnf["qrt"]) - desc = (desc + qrtDisplay + likeDisplay) - except: - vnf['likes'] = 0; vnf['rts'] = 0; vnf['time'] = 0 - print(' ➤ [ X ] Failed QRT check - old VNF object') + if 'poll' in vnf: + pollDisplay= msgs.genPollDisplay(vnf['poll']) + else: + pollDisplay="" + + desc=msgs.formatEmbedDesc(vnf['type'],desc,vnf['qrt'],pollDisplay,likeDisplay) + + print(len(desc)) appNamePost = "" if vnf['type'] == "Text": # Change the template based on tweet type template = 'text.html' @@ -426,10 +430,10 @@ def embed(video_link, vnf, image): image = vnf['images'][image] template = 'image.html' if vnf['type'] == "Video": - urlDesc = urllib.parse.quote(textwrap.shorten(desc, width=220, placeholder="...")) + #urlDesc = urllib.parse.quote(textwrap.shorten(desc, width=220, placeholder="...")) template = 'video.html' if vnf['type'] == "": - urlDesc = urllib.parse.quote(textwrap.shorten(desc, width=220, placeholder="...")) + #urlDesc = urllib.parse.quote(textwrap.shorten(desc, width=220, placeholder="...")) template = 'video.html' color = "#7FFFD4" # Green @@ -477,6 +481,27 @@ def embedCombinedVnf(video_link,vnf): return getTemplate('image.html',vnf,desc,image,video_link,color,urlDesc,urlUser,urlLink,appNameSuffix=" - View original tweet for full quality") +def getPollObject(card): + poll={"total_votes":0,"choices":[]} + choiceCount=0 + if (card["name"]=="poll2choice_text_only"): + choiceCount=2 + elif (card["name"]=="poll3choice_text_only"): + choiceCount=3 + elif (card["name"]=="poll4choice_text_only"): + choiceCount=4 + + for i in range(0,choiceCount): + choice = {"text":card["binding_values"][f"choice{i+1}_label"]["string_value"],"votes":int(card["binding_values"][f"choice{i+1}_count"]["string_value"])} + poll["total_votes"]+=choice["votes"] + poll["choices"].append(choice) + # update each choice with a percentage + for choice in poll["choices"]: + choice["percent"] = round((choice["votes"]/poll["total_votes"])*100,1) + + return poll + + def tweetType(tweet): # Are we dealing with a Video, Image, or Text tweet? if 'extended_entities' in tweet: if 'video_info' in tweet['extended_entities']['media'][0]: