Compare commits
6 Commits
0a347423be
...
72684a1822
Author | SHA1 | Date | |
---|---|---|---|
72684a1822 | |||
6703064cbd | |||
ad931427d0 | |||
de8f69b182 | |||
f1078aa647 | |||
1ed15ef433 |
28
.gitea/workflows/build-get-account.yml
Normal file
28
.gitea/workflows/build-get-account.yml
Normal file
@ -0,0 +1,28 @@
|
||||
name: Build the docker image for the get_account.py script
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["custom"]
|
||||
|
||||
env:
|
||||
REGISTRY: git.ngn.tf
|
||||
IMAGE: ${{gitea.repository}}/get-account
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: "https://github.com/actions/checkout@v4"
|
||||
|
||||
- name: Login to container repo
|
||||
uses: "https://github.com/docker/login-action@v1"
|
||||
with:
|
||||
registry: ${{env.REGISTRY}}
|
||||
username: ${{gitea.actor}}
|
||||
password: ${{secrets.PACKAGES_TOKEN}}
|
||||
|
||||
- name: Build and push the image
|
||||
run: |
|
||||
docker build . -f docker/get_account/Dockerfile --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
||||
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
@ -1,4 +1,4 @@
|
||||
name: Build and publish the docker image
|
||||
name: Build the docker image for the web server
|
||||
|
||||
on:
|
||||
push:
|
||||
@ -6,7 +6,7 @@ on:
|
||||
|
||||
env:
|
||||
REGISTRY: git.ngn.tf
|
||||
IMAGE: ${{gitea.repository}}
|
||||
IMAGE: ${{gitea.repository}}/web
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@ -22,7 +22,7 @@ jobs:
|
||||
username: ${{gitea.actor}}
|
||||
password: ${{secrets.PACKAGES_TOKEN}}
|
||||
|
||||
- name: Build image
|
||||
- name: Build and push the image
|
||||
run: |
|
||||
docker build . --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
||||
docker build . -f docker/nitter/Dockerfile --tag ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
||||
docker push ${{env.REGISTRY}}/${{env.IMAGE}}:latest
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,4 +1,4 @@
|
||||
nitter
|
||||
./nitter
|
||||
*.html
|
||||
*.db
|
||||
data
|
||||
|
@ -1,5 +1,6 @@
|
||||
# [ngn.tf] | nitter
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
A fork of the [nitter](https://github.com/PrivacyDevel/nitter) project, with my personal changes.
|
||||
|
6
docker/get_account/Dockerfile
Normal file
6
docker/get_account/Dockerfile
Normal file
@ -0,0 +1,6 @@
|
||||
FROM python
|
||||
|
||||
RUN pip install pyotp requests
|
||||
COPY ./tools/get_account.py /get_account.py
|
||||
|
||||
ENTRYPOINT ["python3", "/get_account.py"]
|
@ -1,52 +0,0 @@
|
||||
#!/bin/bash -e
|
||||
|
||||
# Grab oauth token for use with Nitter (requires Twitter account).
|
||||
# results: {"oauth_token":"xxxxxxxxxx-xxxxxxxxx","oauth_token_secret":"xxxxxxxxxxxxxxxxxxxxx"}
|
||||
|
||||
if [ $# -ne 2 ]; then
|
||||
echo "please specify a username and password"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
username="${1}"
|
||||
password="${2}"
|
||||
|
||||
bearer_token='AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F'
|
||||
guest_token=$(curl -s -XPOST https://api.twitter.com/1.1/guest/activate.json -H "Authorization: Bearer ${bearer_token}" | jq -r '.guest_token')
|
||||
base_url='https://api.twitter.com/1.1/onboarding/task.json'
|
||||
header=(-H "Authorization: Bearer ${bearer_token}" -H "User-Agent: TwitterAndroid/10.21.1" -H "Content-Type: application/json" -H "X-Guest-Token: ${guest_token}")
|
||||
|
||||
# start flow
|
||||
flow_1=$(curl -si -XPOST "${base_url}?flow_name=login" "${header[@]}")
|
||||
|
||||
# get 'att', now needed in headers, and 'flow_token' from flow_1
|
||||
att=$(sed -En 's/^att: (.*)\r/\1/p' <<< "${flow_1}")
|
||||
flow_token=$(sed -n '$p' <<< "${flow_1}" | jq -r .flow_token)
|
||||
|
||||
if [[ -z "$flow_1" || -z "$flow_token" ]]; then
|
||||
echo "Couldn't retrieve flow token (twitter not reachable?)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# username
|
||||
token_2=$(curl -s -XPOST "${base_url}" -H "att: ${att}" "${header[@]}" \
|
||||
-d '{"flow_token":"'"${flow_token}"'","subtask_inputs":[{"subtask_id":"LoginEnterUserIdentifierSSO","settings_list":{"setting_responses":[{"key":"user_identifier","response_data":{"text_data":{"result":"'"${username}"'"}}}],"link":"next_link"}}]}' | jq -r .flow_token)
|
||||
|
||||
if [[ -z "$token_2" || "$token_2" == "null" ]]; then
|
||||
echo "Couldn't retrieve user token (check if login is correct)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# password
|
||||
token_3=$(curl -s -XPOST "${base_url}" -H "att: ${att}" "${header[@]}" \
|
||||
-d '{"flow_token":"'"${token_2}"'","subtask_inputs":[{"enter_password":{"password":"'"${password}"'","link":"next_link"},"subtask_id":"LoginEnterPassword"}]}' | jq -r .flow_token)
|
||||
|
||||
if [[ -z "$token_3" || "$token_3" == "null" ]]; then
|
||||
echo "Couldn't retrieve user token (check if password is correct)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# finally print oauth_token and secret
|
||||
curl -s -XPOST "${base_url}" -H "att: ${att}" "${header[@]}" \
|
||||
-d '{"flow_token":"'"${token_3}"'","subtask_inputs":[{"check_logged_in_account":{"link":"AccountDuplicationCheck_false"},"subtask_id":"AccountDuplicationCheck"}]}' | \
|
||||
jq -c '.subtasks[0]|if(.open_account) then [{oauth_token: .open_account.oauth_token, oauth_token_secret: .open_account.oauth_token_secret}] else empty end'
|
102
tests/base.py
102
tests/base.py
@ -1,102 +0,0 @@
|
||||
from seleniumbase import BaseCase
|
||||
|
||||
|
||||
class Card(object):
|
||||
def __init__(self, tweet=''):
|
||||
card = tweet + '.card '
|
||||
self.link = card + 'a'
|
||||
self.title = card + '.card-title'
|
||||
self.description = card + '.card-description'
|
||||
self.destination = card + '.card-destination'
|
||||
self.image = card + '.card-image'
|
||||
|
||||
|
||||
class Quote(object):
|
||||
def __init__(self, tweet=''):
|
||||
quote = tweet + '.quote '
|
||||
namerow = quote + '.fullname-and-username '
|
||||
self.link = quote + '.quote-link'
|
||||
self.fullname = namerow + '.fullname'
|
||||
self.username = namerow + '.username'
|
||||
self.text = quote + '.quote-text'
|
||||
self.media = quote + '.quote-media-container'
|
||||
self.unavailable = quote + '.quote.unavailable'
|
||||
|
||||
|
||||
class Tweet(object):
|
||||
def __init__(self, tweet=''):
|
||||
namerow = tweet + '.tweet-header '
|
||||
self.fullname = namerow + '.fullname'
|
||||
self.username = namerow + '.username'
|
||||
self.date = namerow + '.tweet-date'
|
||||
self.text = tweet + '.tweet-content.media-body'
|
||||
self.retweet = tweet + '.retweet-header'
|
||||
self.reply = tweet + '.replying-to'
|
||||
|
||||
|
||||
class Profile(object):
|
||||
fullname = '.profile-card-fullname'
|
||||
username = '.profile-card-username'
|
||||
protected = '.icon-lock'
|
||||
verified = '.verified-icon'
|
||||
banner = '.profile-banner'
|
||||
bio = '.profile-bio'
|
||||
location = '.profile-location'
|
||||
website = '.profile-website'
|
||||
joinDate = '.profile-joindate'
|
||||
mediaCount = '.photo-rail-header'
|
||||
|
||||
|
||||
class Timeline(object):
|
||||
newest = 'div[class="timeline-item show-more"]'
|
||||
older = 'div[class="show-more"]'
|
||||
end = '.timeline-end'
|
||||
none = '.timeline-none'
|
||||
protected = '.timeline-protected'
|
||||
photo_rail = '.photo-rail-grid'
|
||||
|
||||
|
||||
class Conversation(object):
|
||||
main = '.main-tweet'
|
||||
before = '.before-tweet'
|
||||
after = '.after-tweet'
|
||||
replies = '.replies'
|
||||
thread = '.reply'
|
||||
tweet = '.timeline-item'
|
||||
tweet_text = '.tweet-content'
|
||||
|
||||
|
||||
class Poll(object):
|
||||
votes = '.poll-info'
|
||||
choice = '.poll-meter'
|
||||
value = 'poll-choice-value'
|
||||
option = 'poll-choice-option'
|
||||
leader = 'leader'
|
||||
|
||||
|
||||
class Media(object):
|
||||
container = '.attachments'
|
||||
row = '.gallery-row'
|
||||
image = '.still-image'
|
||||
video = '.gallery-video'
|
||||
gif = '.gallery-gif'
|
||||
|
||||
|
||||
class BaseTestCase(BaseCase):
|
||||
def setUp(self):
|
||||
super(BaseTestCase, self).setUp()
|
||||
|
||||
def tearDown(self):
|
||||
super(BaseTestCase, self).tearDown()
|
||||
|
||||
def open_nitter(self, page=''):
|
||||
self.open(f'http://localhost:8080/{page}')
|
||||
|
||||
def search_username(self, username):
|
||||
self.open_nitter()
|
||||
self.update_text('.search-bar input[type=text]', username)
|
||||
self.submit('.search-bar form')
|
||||
|
||||
|
||||
def get_timeline_tweet(num=1):
|
||||
return Tweet(f'.timeline > div:nth-child({num}) ')
|
@ -1 +0,0 @@
|
||||
seleniumbase
|
@ -1,89 +0,0 @@
|
||||
from base import BaseTestCase, Card, Conversation
|
||||
from parameterized import parameterized
|
||||
|
||||
|
||||
card = [
|
||||
['nim_lang/status/1136652293510717440',
|
||||
'Version 0.20.0 released',
|
||||
'We are very proud to announce Nim version 0.20. This is a massive release, both literally and figuratively. It contains more than 1,000 commits and it marks our release candidate for version 1.0!',
|
||||
'nim-lang.org', True],
|
||||
|
||||
['voidtarget/status/1094632512926605312',
|
||||
'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too)',
|
||||
'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too) - obsplugin.nim',
|
||||
'gist.github.com', True],
|
||||
|
||||
['nim_lang/status/1082989146040340480',
|
||||
'Nim in 2018: A short recap',
|
||||
'There were several big news in the Nim world in 2018 – two new major releases, partnership with Status, and much more. But let us go chronologically.',
|
||||
'nim-lang.org', True]
|
||||
]
|
||||
|
||||
no_thumb = [
|
||||
['FluentAI/status/1116417904831029248',
|
||||
'LinkedIn',
|
||||
'This link will take you to a page that’s not on LinkedIn',
|
||||
'lnkd.in'],
|
||||
|
||||
['Thom_Wolf/status/1122466524860702729',
|
||||
'facebookresearch/fairseq',
|
||||
'Facebook AI Research Sequence-to-Sequence Toolkit written in Python. - GitHub - facebookresearch/fairseq: Facebook AI Research Sequence-to-Sequence Toolkit written in Python.',
|
||||
'github.com'],
|
||||
|
||||
['brent_p/status/1088857328680488961',
|
||||
'Hts Nim Sugar',
|
||||
'hts-nim is a library that allows one to use htslib via the nim programming language. Nim is a garbage-collected language that compiles to C and often has similar performance. I have become very...',
|
||||
'brentp.github.io'],
|
||||
|
||||
['voidtarget/status/1133028231672582145',
|
||||
'sinkingsugar/nimqt-example',
|
||||
'A sample of a Qt app written using mostly nim. Contribute to sinkingsugar/nimqt-example development by creating an account on GitHub.',
|
||||
'github.com']
|
||||
]
|
||||
|
||||
playable = [
|
||||
['nim_lang/status/1118234460904919042',
|
||||
'Nim development blog 2019-03',
|
||||
'Arne (aka Krux02)* debugging: * improved nim-gdb, $ works, framefilter * alias for --debugger:native: -g* bugs: * forwarding of .pure. * sizeof union* fe...',
|
||||
'youtube.com'],
|
||||
|
||||
['nim_lang/status/1121090879823986688',
|
||||
'Nim - First natively compiled language w/ hot code-reloading at...',
|
||||
'#nim #c++ #ACCUConfNim is a statically typed systems and applications programming language which offers perhaps some of the most powerful metaprogramming cap...',
|
||||
'youtube.com']
|
||||
]
|
||||
|
||||
class CardTest(BaseTestCase):
|
||||
@parameterized.expand(card)
|
||||
def test_card(self, tweet, title, description, destination, large):
|
||||
self.open_nitter(tweet)
|
||||
c = Card(Conversation.main + " ")
|
||||
self.assert_text(title, c.title)
|
||||
self.assert_text(destination, c.destination)
|
||||
self.assertIn('/pic/', self.get_image_url(c.image + ' img'))
|
||||
if len(description) > 0:
|
||||
self.assert_text(description, c.description)
|
||||
if large:
|
||||
self.assert_element_visible('.card.large')
|
||||
else:
|
||||
self.assert_element_not_visible('.card.large')
|
||||
|
||||
@parameterized.expand(no_thumb)
|
||||
def test_card_no_thumb(self, tweet, title, description, destination):
|
||||
self.open_nitter(tweet)
|
||||
c = Card(Conversation.main + " ")
|
||||
self.assert_text(title, c.title)
|
||||
self.assert_text(destination, c.destination)
|
||||
if len(description) > 0:
|
||||
self.assert_text(description, c.description)
|
||||
|
||||
@parameterized.expand(playable)
|
||||
def test_card_playable(self, tweet, title, description, destination):
|
||||
self.open_nitter(tweet)
|
||||
c = Card(Conversation.main + " ")
|
||||
self.assert_text(title, c.title)
|
||||
self.assert_text(destination, c.destination)
|
||||
self.assertIn('/pic/', self.get_image_url(c.image + ' img'))
|
||||
self.assert_element_visible('.card-overlay')
|
||||
if len(description) > 0:
|
||||
self.assert_text(description, c.description)
|
@ -1,76 +0,0 @@
|
||||
from base import BaseTestCase, Profile
|
||||
from parameterized import parameterized
|
||||
|
||||
profiles = [
|
||||
['mobile_test', 'Test account',
|
||||
'Test Account. test test Testing username with @mobile_test_2 and a #hashtag',
|
||||
'San Francisco, CA', 'example.com/foobar', 'Joined October 2009', '98'],
|
||||
['mobile_test_2', 'mobile test 2', '', '', '', 'Joined January 2011', '13']
|
||||
]
|
||||
|
||||
verified = [['jack'], ['elonmusk']]
|
||||
|
||||
protected = [
|
||||
['mobile_test_7', 'mobile test 7', ''],
|
||||
['Poop', 'Randy', 'Social media fanatic.']
|
||||
]
|
||||
|
||||
invalid = [['thisprofiledoesntexist'], ['%']]
|
||||
|
||||
banner_image = [
|
||||
['mobile_test', 'profile_banners%2F82135242%2F1384108037%2F1500x500']
|
||||
]
|
||||
|
||||
|
||||
class ProfileTest(BaseTestCase):
|
||||
@parameterized.expand(profiles)
|
||||
def test_data(self, username, fullname, bio, location, website, joinDate, mediaCount):
|
||||
self.open_nitter(username)
|
||||
self.assert_exact_text(fullname, Profile.fullname)
|
||||
self.assert_exact_text(f'@{username}', Profile.username)
|
||||
|
||||
tests = [
|
||||
(bio, Profile.bio),
|
||||
(location, Profile.location),
|
||||
(website, Profile.website),
|
||||
(joinDate, Profile.joinDate),
|
||||
(mediaCount + " Photos and videos", Profile.mediaCount)
|
||||
]
|
||||
|
||||
for text, selector in tests:
|
||||
if len(text) > 0:
|
||||
self.assert_exact_text(text, selector)
|
||||
else:
|
||||
self.assert_element_absent(selector)
|
||||
|
||||
@parameterized.expand(verified)
|
||||
def test_verified(self, username):
|
||||
self.open_nitter(username)
|
||||
self.assert_element_visible(Profile.verified)
|
||||
|
||||
@parameterized.expand(protected)
|
||||
def test_protected(self, username, fullname, bio):
|
||||
self.open_nitter(username)
|
||||
self.assert_element_visible(Profile.protected)
|
||||
self.assert_exact_text(fullname, Profile.fullname)
|
||||
self.assert_exact_text(f'@{username}', Profile.username)
|
||||
|
||||
if len(bio) > 0:
|
||||
self.assert_text(bio, Profile.bio)
|
||||
else:
|
||||
self.assert_element_absent(Profile.bio)
|
||||
|
||||
@parameterized.expand(invalid)
|
||||
def test_invalid_username(self, username):
|
||||
self.open_nitter(username)
|
||||
self.assert_text(f'User "{username}" not found')
|
||||
|
||||
def test_suspended(self):
|
||||
self.open_nitter('suspendme')
|
||||
self.assert_text('User "suspendme" has been suspended')
|
||||
|
||||
@parameterized.expand(banner_image)
|
||||
def test_banner_image(self, username, url):
|
||||
self.open_nitter(username)
|
||||
banner = self.find_element(Profile.banner + ' img')
|
||||
self.assertIn(url, banner.get_attribute('src'))
|
@ -1,63 +0,0 @@
|
||||
from base import BaseTestCase, Quote, Conversation
|
||||
from parameterized import parameterized
|
||||
|
||||
text = [
|
||||
['elonmusk/status/1138136540096319488',
|
||||
'TREV PAGE', '@Model3Owners',
|
||||
"""As of March 58.4% of new car sales in Norway are electric.
|
||||
|
||||
What are we doing wrong? reuters.com/article/us-norwa…"""],
|
||||
|
||||
['nim_lang/status/1491461266849808397#m',
|
||||
'Nim', '@nim_lang',
|
||||
"""What's better than Nim 1.6.0?
|
||||
|
||||
Nim 1.6.2 :)
|
||||
|
||||
nim-lang.org/blog/2021/12/17…"""]
|
||||
]
|
||||
|
||||
image = [
|
||||
['elonmusk/status/1138827760107790336', 'D83h6Y8UIAE2Wlz'],
|
||||
['SpaceX/status/1067155053461426176', 'Ds9EYfxXoAAPNmx']
|
||||
]
|
||||
|
||||
gif = [
|
||||
['SpaceX/status/747497521593737216', 'Cl-R5yFWkAA_-3X'],
|
||||
['nim_lang/status/1068099315074248704', 'DtJSqP9WoAAKdRC']
|
||||
]
|
||||
|
||||
video = [
|
||||
['bkuensting/status/1067316003200217088', 'IyCaQlzF0q8u9vBd']
|
||||
]
|
||||
|
||||
|
||||
class QuoteTest(BaseTestCase):
|
||||
@parameterized.expand(text)
|
||||
def test_text(self, tweet, fullname, username, text):
|
||||
self.open_nitter(tweet)
|
||||
quote = Quote(Conversation.main + " ")
|
||||
self.assert_text(fullname, quote.fullname)
|
||||
self.assert_text(username, quote.username)
|
||||
self.assert_text(text, quote.text)
|
||||
|
||||
@parameterized.expand(image)
|
||||
def test_image(self, tweet, url):
|
||||
self.open_nitter(tweet)
|
||||
quote = Quote(Conversation.main + " ")
|
||||
self.assert_element_visible(quote.media)
|
||||
self.assertIn(url, self.get_image_url(quote.media + ' img'))
|
||||
|
||||
@parameterized.expand(gif)
|
||||
def test_gif(self, tweet, url):
|
||||
self.open_nitter(tweet)
|
||||
quote = Quote(Conversation.main + " ")
|
||||
self.assert_element_visible(quote.media)
|
||||
self.assertIn(url, self.get_attribute(quote.media + ' source', 'src'))
|
||||
|
||||
@parameterized.expand(video)
|
||||
def test_video(self, tweet, url):
|
||||
self.open_nitter(tweet)
|
||||
quote = Quote(Conversation.main + " ")
|
||||
self.assert_element_visible(quote.media)
|
||||
self.assertIn(url, self.get_image_url(quote.media + ' img'))
|
@ -1,9 +0,0 @@
|
||||
from base import BaseTestCase
|
||||
from parameterized import parameterized
|
||||
|
||||
|
||||
#class SearchTest(BaseTestCase):
|
||||
#@parameterized.expand([['@mobile_test'], ['@mobile_test_2']])
|
||||
#def test_username_search(self, username):
|
||||
#self.search_username(username)
|
||||
#self.assert_text(f'{username}')
|
@ -1,52 +0,0 @@
|
||||
from base import BaseTestCase, Conversation
|
||||
from parameterized import parameterized
|
||||
|
||||
thread = [
|
||||
['octonion/status/975253897697611777', [], 'Based', ['Crystal', 'Julia'], [
|
||||
['For', 'Then', 'Okay,', 'Python', 'Speed', 'Java', 'Coding', 'I', 'You'],
|
||||
['yeah,']
|
||||
]],
|
||||
|
||||
['octonion/status/975254452625002496', ['Based'], 'Crystal', ['Julia'], []],
|
||||
|
||||
['octonion/status/975256058384887808', ['Based', 'Crystal'], 'Julia', [], []],
|
||||
|
||||
['gauravssnl/status/975364889039417344',
|
||||
['Based', 'For', 'Then', 'Okay,', 'Python'], 'Speed', [], [
|
||||
['Java', 'Coding', 'I', 'You'], ['JAVA!']
|
||||
]],
|
||||
|
||||
['d0m96/status/1141811379407425537', [], 'I\'m',
|
||||
['The', 'The', 'Today', 'Some', 'If', 'There', 'Above'],
|
||||
[['Thank', 'Also,']]],
|
||||
|
||||
['gmpreussner/status/999766552546299904', [], 'A', [],
|
||||
[['I', 'Especially'], ['I']]]
|
||||
]
|
||||
|
||||
|
||||
class ThreadTest(BaseTestCase):
|
||||
def find_tweets(self, selector):
|
||||
return self.find_elements(f"{selector} {Conversation.tweet_text}")
|
||||
|
||||
def compare_first_word(self, tweets, selector):
|
||||
if len(tweets) > 0:
|
||||
self.assert_element_visible(selector)
|
||||
for i, tweet in enumerate(self.find_tweets(selector)):
|
||||
text = tweet.text.split(" ")[0]
|
||||
self.assert_equal(tweets[i], text)
|
||||
|
||||
@parameterized.expand(thread)
|
||||
def test_thread(self, tweet, before, main, after, replies):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_element_visible(Conversation.main)
|
||||
|
||||
self.assert_text(main, Conversation.main)
|
||||
self.assert_text(main, Conversation.main)
|
||||
|
||||
self.compare_first_word(before, Conversation.before)
|
||||
self.compare_first_word(after, Conversation.after)
|
||||
|
||||
for i, reply in enumerate(self.find_elements(Conversation.thread)):
|
||||
selector = Conversation.replies + f" > div:nth-child({i + 1})"
|
||||
self.compare_first_word(replies[i], selector)
|
@ -1,64 +0,0 @@
|
||||
from base import BaseTestCase, Timeline
|
||||
from parameterized import parameterized
|
||||
|
||||
normal = [['jack'], ['elonmusk']]
|
||||
|
||||
after = [['jack', '1681686036294803456'],
|
||||
['elonmusk', '1681686036294803456']]
|
||||
|
||||
no_more = [['mobile_test_8?cursor=DAABCgABF4YVAqN___kKAAICNn_4msIQAAgAAwAAAAIAAA']]
|
||||
|
||||
empty = [['emptyuser'], ['mobile_test_10']]
|
||||
|
||||
protected = [['mobile_test_7'], ['Empty_user']]
|
||||
|
||||
photo_rail = [['mobile_test', ['Bo0nDsYIYAIjqVn', 'BoQbwJAIUAA0QCY', 'BoQbRQxIIAA3FWD', 'Bn8Qh8iIIAABXrG']]]
|
||||
|
||||
|
||||
class TweetTest(BaseTestCase):
|
||||
@parameterized.expand(normal)
|
||||
def test_timeline(self, username):
|
||||
self.open_nitter(username)
|
||||
self.assert_element_present(Timeline.older)
|
||||
self.assert_element_absent(Timeline.newest)
|
||||
self.assert_element_absent(Timeline.end)
|
||||
self.assert_element_absent(Timeline.none)
|
||||
|
||||
@parameterized.expand(after)
|
||||
def test_after(self, username, cursor):
|
||||
self.open_nitter(f'{username}?cursor={cursor}')
|
||||
self.assert_element_present(Timeline.newest)
|
||||
self.assert_element_present(Timeline.older)
|
||||
self.assert_element_absent(Timeline.end)
|
||||
self.assert_element_absent(Timeline.none)
|
||||
|
||||
@parameterized.expand(no_more)
|
||||
def test_no_more(self, username):
|
||||
self.open_nitter(username)
|
||||
self.assert_text('No more items', Timeline.end)
|
||||
self.assert_element_present(Timeline.newest)
|
||||
self.assert_element_absent(Timeline.older)
|
||||
|
||||
@parameterized.expand(empty)
|
||||
def test_empty(self, username):
|
||||
self.open_nitter(username)
|
||||
self.assert_text('No items found', Timeline.none)
|
||||
self.assert_element_absent(Timeline.newest)
|
||||
self.assert_element_absent(Timeline.older)
|
||||
self.assert_element_absent(Timeline.end)
|
||||
|
||||
@parameterized.expand(protected)
|
||||
def test_protected(self, username):
|
||||
self.open_nitter(username)
|
||||
self.assert_text('This account\'s tweets are protected.', Timeline.protected)
|
||||
self.assert_element_absent(Timeline.newest)
|
||||
self.assert_element_absent(Timeline.older)
|
||||
self.assert_element_absent(Timeline.end)
|
||||
|
||||
#@parameterized.expand(photo_rail)
|
||||
#def test_photo_rail(self, username, images):
|
||||
#self.open_nitter(username)
|
||||
#self.assert_element_visible(Timeline.photo_rail)
|
||||
#for i, url in enumerate(images):
|
||||
#img = self.get_attribute(Timeline.photo_rail + f' a:nth-child({i + 1}) img', 'src')
|
||||
#self.assertIn(url, img)
|
@ -1,149 +0,0 @@
|
||||
from base import BaseTestCase, Tweet, Conversation, get_timeline_tweet
|
||||
from parameterized import parameterized
|
||||
|
||||
# image = tweet + 'div.attachments.media-body > div > div > a > div > img'
|
||||
# self.assert_true(self.get_image_url(image).split('/')[0] == 'http')
|
||||
|
||||
timeline = [
|
||||
[1, 'Test account', 'mobile_test', '10 Aug 2016', '763483571793174528',
|
||||
'.'],
|
||||
|
||||
[3, 'Test account', 'mobile_test', '3 Mar 2016', '705522133443571712',
|
||||
'LIVE on #Periscope pscp.tv/w/aadiTzF6dkVOTXZSbX…'],
|
||||
|
||||
[6, 'mobile test 2', 'mobile_test_2', '1 Oct 2014', '517449200045277184',
|
||||
'Testing. One two three four. Test.']
|
||||
]
|
||||
|
||||
status = [
|
||||
[20, 'jack', 'jack', '21 Mar 2006', 'just setting up my twttr'],
|
||||
[134849778302464000, 'The Twoffice', 'TheTwoffice', '11 Nov 2011', 'test'],
|
||||
[105685475985080322, 'The Twoffice', 'TheTwoffice', '22 Aug 2011', 'regular tweet'],
|
||||
[572593440719912960, 'Test account', 'mobile_test', '3 Mar 2015', 'testing test']
|
||||
]
|
||||
|
||||
invalid = [
|
||||
['mobile_test/status/120938109238'],
|
||||
['TheTwoffice/status/8931928312']
|
||||
]
|
||||
|
||||
multiline = [
|
||||
[400897186990284800, 'mobile_test_3',
|
||||
"""
|
||||
♔
|
||||
KEEP
|
||||
CALM
|
||||
AND
|
||||
CLICHÉ
|
||||
ON"""],
|
||||
[1718660434457239868, 'WebDesignMuseum',
|
||||
"""
|
||||
Happy 32nd Birthday HTML tags!
|
||||
|
||||
On October 29, 1991, the internet pioneer, Tim Berners-Lee, published a document entitled HTML Tags.
|
||||
|
||||
The document contained a description of the first 18 HTML tags: <title>, <nextid>, <a>, <isindex>, <plaintext>, <listing>, <p>, <h1>…<h6>, <address>, <hp1>, <hp2>…, <dl>, <dt>, <dd>, <ul>, <li>,<menu> and <dir>. The design of the first version of HTML language was influenced by the SGML universal markup language.
|
||||
|
||||
#WebDesignHistory"""]
|
||||
]
|
||||
|
||||
link = [
|
||||
['nim_lang/status/1110499584852353024', [
|
||||
'nim-lang.org/araq/ownedrefs.…',
|
||||
'news.ycombinator.com/item?id…',
|
||||
'teddit.net/r/programming…'
|
||||
]],
|
||||
['nim_lang/status/1125887775151140864', [
|
||||
'en.wikipedia.org/wiki/Nim_(p…'
|
||||
]],
|
||||
['hiankun_taioan/status/1086916335215341570', [
|
||||
'(hackernoon.com/interview-wit…)'
|
||||
]],
|
||||
['archillinks/status/1146302618223951873', [
|
||||
'flickr.com/photos/87101284@N…',
|
||||
'hisafoto.tumblr.com/post/176…'
|
||||
]],
|
||||
['archillinks/status/1146292551936335873', [
|
||||
'flickr.com/photos/michaelrye…',
|
||||
'furtho.tumblr.com/post/16618…'
|
||||
]]
|
||||
]
|
||||
|
||||
username = [
|
||||
['Bountysource/status/1094803522053320705', ['nim_lang']],
|
||||
['leereilly/status/1058464250098704385', ['godotengine', 'unity3d', 'nim_lang']]
|
||||
]
|
||||
|
||||
emoji = [
|
||||
['Tesla/status/1134850442511257600', '🌈❤️🧡💛💚💙💜']
|
||||
]
|
||||
|
||||
retweet = [
|
||||
[7, 'mobile_test_2', 'mobile test 2', 'Test account', '@mobile_test', '1234'],
|
||||
[3, 'mobile_test_8', 'mobile test 8', 'jack', '@jack', 'twttr']
|
||||
]
|
||||
|
||||
|
||||
class TweetTest(BaseTestCase):
|
||||
@parameterized.expand(timeline)
|
||||
def test_timeline(self, index, fullname, username, date, tid, text):
|
||||
self.open_nitter(username)
|
||||
tweet = get_timeline_tweet(index)
|
||||
self.assert_exact_text(fullname, tweet.fullname)
|
||||
self.assert_exact_text('@' + username, tweet.username)
|
||||
self.assert_exact_text(date, tweet.date)
|
||||
self.assert_text(text, tweet.text)
|
||||
permalink = self.find_element(tweet.date + ' a')
|
||||
self.assertIn(tid, permalink.get_attribute('href'))
|
||||
|
||||
@parameterized.expand(status)
|
||||
def test_status(self, tid, fullname, username, date, text):
|
||||
tweet = Tweet()
|
||||
self.open_nitter(f'{username}/status/{tid}')
|
||||
self.assert_exact_text(fullname, tweet.fullname)
|
||||
self.assert_exact_text('@' + username, tweet.username)
|
||||
self.assert_exact_text(date, tweet.date)
|
||||
self.assert_text(text, tweet.text)
|
||||
|
||||
@parameterized.expand(multiline)
|
||||
def test_multiline_formatting(self, tid, username, text):
|
||||
self.open_nitter(f'{username}/status/{tid}')
|
||||
self.assert_text(text.strip('\n'), Conversation.main)
|
||||
|
||||
@parameterized.expand(emoji)
|
||||
def test_emoji(self, tweet, text):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_text(text, Conversation.main)
|
||||
|
||||
@parameterized.expand(link)
|
||||
def test_link(self, tweet, links):
|
||||
self.open_nitter(tweet)
|
||||
for link in links:
|
||||
self.assert_text(link, Conversation.main)
|
||||
|
||||
@parameterized.expand(username)
|
||||
def test_username(self, tweet, usernames):
|
||||
self.open_nitter(tweet)
|
||||
for un in usernames:
|
||||
link = self.find_link_text(f'@{un}')
|
||||
self.assertIn(f'/{un}', link.get_property('href'))
|
||||
|
||||
@parameterized.expand(retweet)
|
||||
def test_retweet(self, index, url, retweet_by, fullname, username, text):
|
||||
self.open_nitter(url)
|
||||
tweet = get_timeline_tweet(index)
|
||||
self.assert_text(f'{retweet_by} retweeted', tweet.retweet)
|
||||
self.assert_text(text, tweet.text)
|
||||
self.assert_exact_text(fullname, tweet.fullname)
|
||||
self.assert_exact_text(username, tweet.username)
|
||||
|
||||
@parameterized.expand(invalid)
|
||||
def test_invalid_id(self, tweet):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_text('Tweet not found', '.error-panel')
|
||||
|
||||
#@parameterized.expand(reply)
|
||||
#def test_thread(self, tweet, num):
|
||||
#self.open_nitter(tweet)
|
||||
#thread = self.find_element(f'.timeline > div:nth-child({num})')
|
||||
#self.assertIn(thread.get_attribute('class'), 'thread-line')
|
@ -1,113 +0,0 @@
|
||||
from base import BaseTestCase, Poll, Media
|
||||
from parameterized import parameterized
|
||||
from selenium.webdriver.common.by import By
|
||||
|
||||
poll = [
|
||||
['nim_lang/status/1064219801499955200', 'Style insensitivity', '91', 1, [
|
||||
('47%', 'Yay'), ('53%', 'Nay')
|
||||
]],
|
||||
|
||||
['polls/status/1031986180622049281', 'What Tree Is Coolest?', '3,322', 1, [
|
||||
('30%', 'Oak'), ('42%', 'Bonsai'), ('5%', 'Hemlock'), ('23%', 'Apple')
|
||||
]]
|
||||
]
|
||||
|
||||
image = [
|
||||
['mobile_test/status/519364660823207936', 'BzUnaDFCUAAmrjs'],
|
||||
#['mobile_test_2/status/324619691039543297', 'BIFH45vCUAAQecj']
|
||||
]
|
||||
|
||||
gif = [
|
||||
['elonmusk/status/1141367104702038016', 'D9bzUqoUcAAfUgf'],
|
||||
['Proj_Borealis/status/1136595194621677568', 'D8X_PJAXUAAavPT']
|
||||
]
|
||||
|
||||
video_m3u8 = [
|
||||
['d0m96/status/1078373829917974528', '9q1-v9w8-ft3awgD.jpg'],
|
||||
['SpaceX/status/1138474014152712192', 'ocJJj2uu4n1kyD2Y.jpg']
|
||||
]
|
||||
|
||||
gallery = [
|
||||
# ['mobile_test/status/451108446603980803', [
|
||||
# ['BkKovdrCUAAEz79', 'BkKovdcCEAAfoBO']
|
||||
# ]],
|
||||
|
||||
# ['mobile_test/status/471539824713691137', [
|
||||
# ['Bos--KNIQAAA7Li', 'Bos--FAIAAAWpah'],
|
||||
# ['Bos--IqIQAAav23']
|
||||
# ]],
|
||||
|
||||
['mobile_test/status/469530783384743936', [
|
||||
['BoQbwJAIUAA0QCY', 'BoQbwN1IMAAuTiP'],
|
||||
['BoQbwarIAAAlaE-', 'BoQbwh_IEAA27ef']
|
||||
]]
|
||||
]
|
||||
|
||||
|
||||
class MediaTest(BaseTestCase):
|
||||
@parameterized.expand(poll)
|
||||
def test_poll(self, tweet, text, votes, leader, choices):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_text(text, '.main-tweet')
|
||||
self.assert_text(votes, Poll.votes)
|
||||
|
||||
poll_choices = self.find_elements(Poll.choice)
|
||||
for i, (v, o) in enumerate(choices):
|
||||
choice = poll_choices[i]
|
||||
value = choice.find_element(By.CLASS_NAME, Poll.value)
|
||||
option = choice.find_element(By.CLASS_NAME, Poll.option)
|
||||
choice_class = choice.get_attribute('class')
|
||||
|
||||
self.assert_equal(v, value.text)
|
||||
self.assert_equal(o, option.text)
|
||||
|
||||
if i == leader:
|
||||
self.assertIn(Poll.leader, choice_class)
|
||||
else:
|
||||
self.assertNotIn(Poll.leader, choice_class)
|
||||
|
||||
@parameterized.expand(image)
|
||||
def test_image(self, tweet, url):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_element_visible(Media.container)
|
||||
self.assert_element_visible(Media.image)
|
||||
|
||||
image_url = self.get_image_url(Media.image + ' img')
|
||||
self.assertIn(url, image_url)
|
||||
|
||||
@parameterized.expand(gif)
|
||||
def test_gif(self, tweet, gif_id):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_element_visible(Media.container)
|
||||
self.assert_element_visible(Media.gif)
|
||||
|
||||
url = self.get_attribute('source', 'src')
|
||||
thumb = self.get_attribute('video', 'poster')
|
||||
self.assertIn(gif_id + '.mp4', url)
|
||||
self.assertIn(gif_id + '.jpg', thumb)
|
||||
|
||||
@parameterized.expand(video_m3u8)
|
||||
def test_video_m3u8(self, tweet, thumb):
|
||||
# no url because video playback isn't supported yet
|
||||
self.open_nitter(tweet)
|
||||
self.assert_element_visible(Media.container)
|
||||
self.assert_element_visible(Media.video)
|
||||
|
||||
video_thumb = self.get_attribute(Media.video + ' img', 'src')
|
||||
self.assertIn(thumb, video_thumb)
|
||||
|
||||
@parameterized.expand(gallery)
|
||||
def test_gallery(self, tweet, rows):
|
||||
self.open_nitter(tweet)
|
||||
self.assert_element_visible(Media.container)
|
||||
self.assert_element_visible(Media.row)
|
||||
self.assert_element_visible(Media.image)
|
||||
|
||||
gallery_rows = self.find_elements(Media.row)
|
||||
self.assert_equal(len(rows), len(gallery_rows))
|
||||
|
||||
for i, row in enumerate(gallery_rows):
|
||||
images = row.find_elements(By.CSS_SELECTOR, 'img')
|
||||
self.assert_equal(len(rows[i]), len(images))
|
||||
for j, image in enumerate(images):
|
||||
self.assertIn(rows[i][j], image.get_attribute('src'))
|
159
tools/get_account.py
Normal file
159
tools/get_account.py
Normal file
@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env python3
|
||||
from sys import argv
|
||||
import requests
|
||||
import pyotp
|
||||
import json
|
||||
|
||||
TW_CONSUMER_KEY = "3nVuSoBZnx6U4vzUxf5w"
|
||||
TW_CONSUMER_SECRET = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"
|
||||
|
||||
|
||||
def auth(username, password, otp_secret):
|
||||
bearer_token_req = requests.post(
|
||||
"https://api.twitter.com/oauth2/token",
|
||||
auth=(TW_CONSUMER_KEY, TW_CONSUMER_SECRET),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
data="grant_type=client_credentials",
|
||||
).json()
|
||||
bearer_token = " ".join(str(x) for x in bearer_token_req.values())
|
||||
|
||||
guest_token = (
|
||||
requests.post(
|
||||
"https://api.twitter.com/1.1/guest/activate.json",
|
||||
headers={"Authorization": bearer_token},
|
||||
)
|
||||
.json()
|
||||
.get("guest_token")
|
||||
)
|
||||
|
||||
if not guest_token:
|
||||
print("Failed to obtain guest token.")
|
||||
exit(1)
|
||||
|
||||
twitter_header = {
|
||||
"Authorization": bearer_token,
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": "TwitterAndroid/10.21.0-release.0 (310210000-r-0) ONEPLUS+A3010/9 (OnePlus;ONEPLUS+A3010;OnePlus;OnePlus3;0;;1;2016)",
|
||||
"X-Twitter-API-Version": "5",
|
||||
"X-Twitter-Client": "TwitterAndroid",
|
||||
"X-Twitter-Client-Version": "10.21.0-release.0",
|
||||
"OS-Version": "28",
|
||||
"System-User-Agent": "Dalvik/2.1.0 (Linux; U; Android 9; ONEPLUS A3010 Build/PKQ1.181203.001)",
|
||||
"X-Twitter-Active-User": "yes",
|
||||
"X-Guest-Token": guest_token,
|
||||
"X-Twitter-Client-DeviceID": "",
|
||||
}
|
||||
|
||||
session = requests.Session()
|
||||
session.headers = twitter_header
|
||||
|
||||
task1 = session.post(
|
||||
"https://api.twitter.com/1.1/onboarding/task.json",
|
||||
params={
|
||||
"flow_name": "login",
|
||||
"api_version": "1",
|
||||
"known_device_token": "",
|
||||
"sim_country_code": "us",
|
||||
},
|
||||
json={
|
||||
"flow_token": None,
|
||||
"input_flow_data": {
|
||||
"country_code": None,
|
||||
"flow_context": {
|
||||
"referrer_context": {
|
||||
"referral_details": "utm_source=google-play&utm_medium=organic",
|
||||
"referrer_url": "",
|
||||
},
|
||||
"start_location": {"location": "deeplink"},
|
||||
},
|
||||
"requested_variant": None,
|
||||
"target_user_id": 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
session.headers["att"] = task1.headers.get("att")
|
||||
|
||||
task2 = session.post(
|
||||
"https://api.twitter.com/1.1/onboarding/task.json",
|
||||
json={
|
||||
"flow_token": task1.json().get("flow_token"),
|
||||
"subtask_inputs": [
|
||||
{
|
||||
"enter_text": {
|
||||
"suggestion_id": None,
|
||||
"text": username,
|
||||
"link": "next_link",
|
||||
},
|
||||
"subtask_id": "LoginEnterUserIdentifier",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
task3 = session.post(
|
||||
"https://api.twitter.com/1.1/onboarding/task.json",
|
||||
json={
|
||||
"flow_token": task2.json().get("flow_token"),
|
||||
"subtask_inputs": [
|
||||
{
|
||||
"enter_password": {"password": password, "link": "next_link"},
|
||||
"subtask_id": "LoginEnterPassword",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
|
||||
for t3_subtask in task3.json().get("subtasks", []):
|
||||
if "open_account" in t3_subtask:
|
||||
return t3_subtask["open_account"]
|
||||
elif "enter_text" in t3_subtask:
|
||||
response_text = t3_subtask["enter_text"]["hint_text"]
|
||||
totp = pyotp.TOTP(otp_secret)
|
||||
generated_code = totp.now()
|
||||
task4resp = session.post(
|
||||
"https://api.twitter.com/1.1/onboarding/task.json",
|
||||
json={
|
||||
"flow_token": task3.json().get("flow_token"),
|
||||
"subtask_inputs": [
|
||||
{
|
||||
"enter_text": {
|
||||
"suggestion_id": None,
|
||||
"text": generated_code,
|
||||
"link": "next_link",
|
||||
},
|
||||
"subtask_id": "LoginTwoFactorAuthChallenge",
|
||||
}
|
||||
],
|
||||
},
|
||||
)
|
||||
task4 = task4resp.json()
|
||||
for t4_subtask in task4.get("subtasks", []):
|
||||
if "open_account" in t4_subtask:
|
||||
return t4_subtask["open_account"]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(argv) != 4:
|
||||
print("Usage: %s <username> <password> <2fa secret>" % argv[0])
|
||||
exit(1)
|
||||
|
||||
username = argv[1]
|
||||
password = argv[2]
|
||||
otp_secret = argv[3]
|
||||
|
||||
result = auth(username, password, otp_secret)
|
||||
if result is None:
|
||||
print("Authentication failed.")
|
||||
exit(1)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"oauth_token": result.get("oauth_token"),
|
||||
"oauth_token_secret": result.get("oauth_token_secret"),
|
||||
}
|
||||
)
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user