diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0cffcb3 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +config.json \ No newline at end of file diff --git a/autodeny.py b/autodeny.py index 10165a8..fe8077e 100644 --- a/autodeny.py +++ b/autodeny.py @@ -1,13 +1,21 @@ #!/usr/bin/python3.8 -from mastodon import Mastodon import sys import os import re -import sys import argparse import time +import json +import requests -parser = argparse.ArgumentParser(description='Remove follow requests that match the following filters:\n1: Has less than n (defaults to 1) posts\n2: Has no profile picture\n3: Has no bio\n\nAdding extra filters is somewhat easy if you are familiar with python as well as the mastodon.py wrapper.', formatter_class=argparse.RawTextHelpFormatter) +parent = os.path.dirname(os.path.realpath(__file__)) +if not os.path.exists(os.path.join(parent, 'config.json')): + generate_config("Wardyn's feditools", "read write follow push") +with open(os.path.join(parent, 'config.json'), 'r') as config_file: + config = json.load(config_file) +session = requests.Session() +session.headers.update({"Authorization" : "Bearer " + config['user_token']}) + +parser = argparse.ArgumentParser(description='Remove follow requests that match the following filters:\n1: Has less than n (defaults to 1) posts\n2: Has no profile picture\n3: Has no bio\n', formatter_class=argparse.RawTextHelpFormatter) parser.add_argument('-t', '--threshold', action='store', type=int, @@ -22,6 +30,13 @@ parser.add_argument('-i', '--instances', default=None, dest='instances' ) +parser.add_argument('-b', '--blank', + action='store', + type=str, + default=None, + help='If your instance has a custom image for blank pfps set, please specify the url to the image with this flag', + dest='custompfp' +) parser.add_argument('-p', '--posts', action='store', type=int, @@ -65,81 +80,56 @@ simulate = args.simulate accept= args.accept loop = args.loop instances = args.instances +custompfp = args.custompfp if instances is not None: instances = instances.split(", ") if type(loop) == int: loop = loop * 60 * 60 -# Ensure Fediverse credentials -parent = os.path.dirname(os.path.realpath(__file__)) -if os.path.exists(os.path.join(parent, '.creds')) == False: - os.mkdir(os.path.join(parent, '.creds')) +blanks = [config['instance'] + '/avatars/original/missing.png', config['instance'] + '/images/avi.png'] +if custompfp: + blanks.append(custompfp) -if os.path.exists(os.path.join(parent, '.creds', 'client.secret')) == False: - instance = input('Please enter your instance: ') - if not instance[:4] == 'http': - instance = 'https://' + instance - Mastodon.create_app('Wardyns fedi tools', api_base_url = instance, to_file = os.path.join(parent, '.creds', 'client.secret')) -mastodon = Mastodon( - client_id = os.path.join(parent, '.creds', 'client.secret'), -) - -if os.path.exists(os.path.join(parent, '.creds', 'user.secret')) == False: - username = input('Enter your username: ') - password = input('Enter your password: ') - mastodon.log_in(username=username, password=password, scopes=['read', 'write'], to_file=os.path.join(parent, '.creds', 'user.secret')) - -mastodon = Mastodon( - access_token = os.path.join(parent, '.creds', 'user.secret') -) - -def filterposts(min, request): - posts = request['statuses_count'] +def filterposts(min, follow_request): + posts = follow_request['statuses_count'] if posts >= min: return(True) else: return(False) -def filterbio(request): - if request['note'] == '

': +def filterbio(follow_request): + if follow_request['note'] == '

': return(False) else: return(True) -def filterpfp(request): - if request['avatar'] == mastodon.api_base_url + '/avatars/original/missing.png' or request['avatar'] == mastodon.api_base_url + '/images/avi.png': +def filterpfp(follow_request): + if follow_request['avatar'] in blanks: return(False) else: return(True) while True: denied=[] accepted=[] - while True: - try: - requests = mastodon.follow_requests() - break - except: - continue - if len(requests) > 0: - for request in requests: - #print(request['fqn']) - #print(request['id']) - postcheck = filterposts(minposts, request) - pfpcheck = filterpfp(request) - biocheck = filterbio(request) - #print('Has more than one post?: ' + str(postcheck)) - #print('Has a pfp?: ' + str(pfpcheck)) - #print('Has a bio?: ' + str(biocheck)) - following = mastodon.account_relationships(request)[0]['following'] + + follow_requests = session.get(config['instance'] + '/api/v1/follow_requests', params={'limit':80}).json() + + if len(follow_requests) > 0: + for follow_request in follow_requests: + postcheck = filterposts(minposts, follow_request) + pfpcheck = filterpfp(follow_request) + biocheck = filterbio(follow_request) + + following = session.get(config['instance'] + '/api/v1/accounts/relationships', params={'id':follow_request['id']}).json()[0]['following'] blockedinstance = False if instances is not None: - blockedinstance = request['fqn'].split('@')[-1] in instances + blockedinstance = follow_request['fqn'].split('@')[-1] in instances if postcheck + biocheck + pfpcheck < threshold and following == False or blockedinstance == True: - denied.append(request) + denied.append(follow_request) elif accept == True: - accepted.append(request) + accepted.append(follow_request) print('DENIED: ') - for request in denied: - print(request['fqn']) + for follow_request in denied: + print(follow_request['fqn']) confirm = None if auto == False: while True: @@ -151,16 +141,16 @@ while True: continue break if auto == True or confirm == 'y': - for request in denied: + for follow_request in denied: if simulate: - print('This is where ' + request['fqn'] + ' would be rejected') + print('This is where ' + follow_request['fqn'] + ' would be rejected') else: - mastodon.follow_request_reject(request['id']) - for request in accepted: + session.post(config['instance'] + '/api/v1/follow_requests/' + follow_request['id'] + '/reject') + for follow_request in accepted: if simulate: - print('This is where ' + request['fqn'] + ' would be accepted') + print('This is where ' + follow_request['fqn'] + ' would be accepted') else: - mastodon.follow_request_authorize(request['id']) + session.post(config['instance'] + '/api/v1/follow_requests/' + follow_request['id'] + '/authorize') if loop == None: break else: diff --git a/config_template.json b/config_template.json new file mode 100644 index 0000000..71552b7 --- /dev/null +++ b/config_template.json @@ -0,0 +1,6 @@ +{ + "instance":"", + "client_id":"", + "client_secret":"", + "user_token":"" +} diff --git a/fedisearch.py b/fedisearch.py index 486801a..e4a7ab4 100644 --- a/fedisearch.py +++ b/fedisearch.py @@ -1,9 +1,19 @@ # Import modules -from mastodon import Mastodon import os import html2text from argparse import ArgumentParser -import os +import json +import requests + +parent = os.path.dirname(os.path.realpath(__file__)) + +if not os.path.exists(os.path.join(parent, 'config.json')): + generate_config("Wardyn's feditools", "read write follow push") +with open(os.path.join(parent, 'config.json'), 'r') as config_file: + config = json.load(config_file) + +session = requests.Session() +session.headers.update({"Authorization" : "Bearer " + config['user_token']}) # Initialize arguments parser = ArgumentParser(description='Search a fedi users posts for a specific word or phrase') @@ -36,30 +46,6 @@ dms = args.dms account = args.account pattern = args.pattern -# Ensure Fediverse credentials -parent = os.path.dirname(os.path.realpath(__file__)) -if os.path.exists(os.path.join(parent, '.creds')) == False: - os.mkdir(os.path.join(parent, '.creds')) - -if os.path.exists(os.path.join(parent, '.creds', 'client.secret')) == False: - instance = input('Please enter your instance: ') - if not instance[:4] == 'http': - instance = 'https://' + instance - Mastodon.create_app('Wardyns fedi tools', api_base_url = instance, to_file = os.path.join(parent, '.creds', 'client.secret')) - -mastodon = Mastodon( - client_id = os.path.join(parent, '.creds', 'client.secret'), -) - -if os.path.exists(os.path.join(parent, '.creds', 'user.secret')) == False: - username = input('Enter your username: ') - password = input('Enter your password: ') - mastodon.log_in(username=username, password=password, scopes=['read', 'write'], to_file=os.path.join(parent, '.creds', 'user.secret')) - -mastodon = Mastodon( - access_token = os.path.join(parent, '.creds', 'user.secret') -) - # Main block if case == False: pattern = pattern.lower() @@ -70,8 +56,8 @@ htmlconvert.body_width = 0 if account[0] == '@': account = account[1:] if len(account.split('@')) == 1: - account = account + '@' + mastodon.api_base_url.split('/')[2] -accountlist = mastodon.account_search(account) + account = account + '@' + config['instance'].split('/')[2] +accountlist = session.get(config['instance'] + '/api/v2/search', params={'q':account}).json()['accounts'] for curaccount in accountlist: print(curaccount['fqn']) @@ -85,20 +71,11 @@ accid = account['id'] print('Searching for posts including "' + pattern + '" from user ' + account['fqn']) print('\n---\n') while True: - while True: - try: - statuses = mastodon.account_statuses(accid, max_id=oldest_status_id, limit=1000) - break - except IndexError: - break - except: - pass - if len(statuses) == 0: - break + statuses = session.get(config['instance'] + '/api/v1/accounts/' + accid + '/statuses', params={'max_id':oldest_status_id, 'limit':40}).json() oldest_status_id = statuses[-1]['id'] for status in statuses: if status['reblog'] == None: - if status['visibility'] == 'direct' and dms == False: + if status['visibility'] == 'direct' and not dms: continue content = str(htmlconvert.handle(status['content'])) if case == False: @@ -107,4 +84,6 @@ while True: print(content) print('\nlink: ' + status['url']) print('\n---\n') + if len(statuses) < 40: + break print('Finished searching') \ No newline at end of file diff --git a/generate_config.py b/generate_config.py new file mode 100644 index 0000000..208ad3b --- /dev/null +++ b/generate_config.py @@ -0,0 +1,34 @@ +import os +import requests +import json +from urllib.parse import urlencode + +def generate_config(app_name, scopes): + #Ensure Credentials + parent = os.path.dirname(os.path.realpath(__file__)) + with open(os.path.join(parent, 'config_template.json'), 'r') as template: + config = json.load(template) + + #Create app + instance = input('Please enter your instance: ') + if not instance[:4] == 'http': + instance = 'https://' + instance + + response = requests.post(instance + '/api/v1/apps', data={'client_name':app_name,'scopes':scopes, 'redirect_uris':'urn:ietf:wg:oauth:2.0:oob'}).json() + + client_id = config['client_id'] = response['client_id'] + client_secret = config['client_secret'] = response['client_secret'] + config['instance'] = instance + + #Log in to user account + + print(instance + '/oauth/authorize?', urlencode({'response_type':'code', 'client_id':client_id, 'redirect_uri':'urn:ietf:wg:oauth:2.0:oob', 'scope':scopes})) + code = input("To generate a token to access your account, " + app_name + " needs an authorization code. Please authorize using the link above and enter the code it provides you \nCode: ") + response = requests.post(instance + '/oauth/token', data={'grant_type':'authorization_code', 'code':code, 'client_id':client_id, 'client_secret':client_secret, 'redirect_uri':'urn:ietf:wg:oauth:2.0:oob', 'scope':scopes}) + config['user_token'] = response.json()['access_token'] + + with open(os.path.join(parent, 'config.json'), 'w') as config_file: + config_file.write(json.dumps(config)) + +if __name__ == "__main__": + generate_config("test_app", "read") \ No newline at end of file diff --git a/highestpost.py b/highestpost.py index e7df895..a00ceb4 100644 --- a/highestpost.py +++ b/highestpost.py @@ -1,55 +1,59 @@ #!/usr/bin/python3.8 -from mastodon import Mastodon import os import sys import html2text +import json +import requests +parent = os.path.dirname(os.path.realpath(__file__)) -if os.path.exists('./.creds/') == False: - os.mkdir('./.creds') -if os.path.exists('./.creds/client.secret') == False: - instance = input('Please enter your instance: ') - if not instance[:4] == 'http': - instance = 'https://' + instance - Mastodon.create_app('Wardyns fedi tools', api_base_url = instance, to_file = './.creds/client.secret') +if not os.path.exists(os.path.join(parent, 'config.json')): + generate_config("Wardyn's feditools", "read write follow push") +with open(os.path.join(parent, 'config.json'), 'r') as config_file: + config = json.load(config_file) +session = requests.Session() +session.headers.update({"Authorization" : "Bearer " + config['user_token']}) -mastodon = Mastodon( - client_id = './.creds/client.secret', -) - -if os.path.exists('./.creds/user.secret') == False: - username = input('Enter your username: ') - password = input('Enter your password: ') - mastodon.log_in(username=username, password=password, scopes=['read', 'write'], to_file='./.creds/user.secret') - - -mastodon = Mastodon( - access_token = './.creds/user.secret' -) -if len(sys.argv) < 2: +if len(sys.argv) != 2: print('Expected 1 argument (account)') quit() -else: - highestpost = None - oldest_status_id = None - htmlconvert = html2text.HTML2Text() - htmlconvert.ignore_links = True - htmlconvert.body_width = 0 - account = sys.argv[1] - accid = mastodon.account_search(account)[0]['id'] - while True: - statuses = mastodon.account_statuses(accid, max_id=oldest_status_id, limit=1000) - try: - oldest_status_id = statuses[-1]['id'] - except IndexError: - print('Reached end of posts') - quit() - for status in statuses: - statusscore = status['reblogs_count'] + status['favourites_count'] - if status['reblogged'] == False: - if highestpost == None or statusscore > highestpost['score']: - highestpost = {'score' : status['reblogs_count'] + status['favourites_count'], 'post' : status} - content = str(htmlconvert.handle(status['content'])) - print(content) - print('Favorites: ' + str(status['favourites_count']) + ' Boosts: ' + str(status['reblogs_count'])) - print('link: ' + status['url']) - print('-------') \ No newline at end of file + +highestpost = None +oldest_status_id = None +htmlconvert = html2text.HTML2Text() +htmlconvert.ignore_links = True +htmlconvert.body_width = 0 +account = sys.argv[1] + +if account[0] == '@': + account = account[1:] +if len(account.split('@')) == 1: + account = account + '@' + config['instance'].split('/')[-1] + +searchlist = session.get(config['instance'] + '/api/v2/search?', params={'q':account, 'type':'accounts'}).json()['accounts'] +acc_id = None +for user in searchlist: + if user['fqn'].lower() == account.lower(): + acc_id = user['id'] + acc_name = user['fqn'] + break +if not acc_id: + print("Could not find user: " + account) + quit() + +#accid = session.get(config['instance'] + '/api/v2/search?', params={'q':account, 'type':'accounts'}).json()['accounts'][0]['id'] +while True: + statuses = session.get(config['instance'] + '/api/v1/accounts/' + acc_id + '/statuses', params={"max_id":oldest_status_id, "limit":40, "exclude_reblogs":True}).json() + try: + oldest_status_id = statuses[-1]['id'] + except IndexError: + print('Reached end of posts') + quit() + for status in statuses: + status_score = status['reblogs_count'] + status['favourites_count'] + if highestpost == None or status_score > highestpost['score']: + highestpost = {'score' : status['reblogs_count'] + status['favourites_count'], 'post' : status} + content = str(htmlconvert.handle(status['content'])) + print(content) + print('Favorites: ' + str(status['favourites_count']) + ' Boosts: ' + str(status['reblogs_count'])) + print('link: ' + status['url']) + print('-------') \ No newline at end of file diff --git a/import_following.py b/import_following.py new file mode 100644 index 0000000..98dbfa8 --- /dev/null +++ b/import_following.py @@ -0,0 +1,45 @@ +import requests +import json +import os +from argparse import ArgumentParser, FileType +from generate_config import generate_config + +parent = os.path.dirname(os.path.realpath(__file__)) + +# Initialize arguments +parser = ArgumentParser(description='Import follows from a csv file') +parser.add_argument('file', + type=open, + help='csv file containing users to follow', +) +args = parser.parse_args() +csv = args.file.read() + +if not os.path.exists(os.path.join(parent, 'config.json')): + generate_config("Wardyn's feditools", "read write follow push") +with open(os.path.join(parent, 'config.json'), 'r') as config_file: + config = json.load(config_file) + +session = requests.Session() +session.headers.update({"Authorization" : "Bearer " + config['user_token']}) + + +acclist = csv.split('\n') +for account in acclist: + searchlist = session.get(config['instance'] + '/api/v2/search?', params={'q':account, 'type':'accounts'}).json() + searchlist = searchlist['accounts'] + found = False + for user in searchlist: + if user['fqn'] == account: + account = user + found = True + break + if found == False: + print("Could not find user: " + account) + continue + relationship = account['pleroma']['relationship'] + if relationship['following'] == True: + print('Already following: ' + account['fqn']) + continue + print("Following: " + account['fqn']) + session.post(config['instance'] + '/api/v1/accounts/' + account['id'] + '/follow') diff --git a/notification.mp3 b/notification.mp3 new file mode 100644 index 0000000..bf9c3c1 Binary files /dev/null and b/notification.mp3 differ diff --git a/notification_sound.py b/notification_sound.py new file mode 100644 index 0000000..34152a7 --- /dev/null +++ b/notification_sound.py @@ -0,0 +1,30 @@ +import websocket +import os +from playsound import playsound +from generate_config import generate_config +import json + +parent = os.path.dirname(os.path.realpath(__file__)) + +if not os.path.exists(os.path.join(parent, 'config.json')): + generate_config("Wardyn's feditools", "read write follow push") +with open(os.path.join(parent, 'config.json'), 'r') as config_file: + config = json.load(config_file) + +sound = os.path.join(parent, 'notification.mp3') + +def on_message(ws, message): + print('Received notification') + playsound(sound) +def on_open(ws): + print('Websocket opened') +def on_close(ws): + print('Websocket closed, attempting to reconnect') + connect_websocket() + +def connect_websocket(): + ws = websocket.WebSocketApp("wss://" + config['instance'].split('/')[-1] + "/api/v1/streaming?access_token=" + config['user_token'] + "&stream=user:notification", + on_message = on_message, on_open = on_open, on_close = on_close, on_error=on_close) + ws.run_forever() + +connect_websocket() \ No newline at end of file