๐พ Archived View for m0yng.uk โบ 2022 โบ 05 โบ mastoWelcomer captured on 2023-04-26 at 13:06:39. Gemini links have been rewritten to link to archived content
โฌ ๏ธ Previous capture (2023-01-29)
โก๏ธ Next capture (2023-05-24)
-=-=-=-=-=-=-
Created 2022-05-22
Modified 2022-11-18
[TOC]
I run mastodon.radio, and I have it set to require approval for new accounts. I also like to welcome every new user.
This can result in delays, as I need to do a complex dance, something like:
I wrote a snippet of JavaScript to run in the dev console to copy out the selected usernames, which helped but for security reasons you can't put stuff on the clipboard without user interaction so I still had to manually select and copy the string... the entire process is a faff.
Well, mastodon has an API, and there exists Mastodon.py
Guess what comes next?
That's right! SCOPE CREEP!
I wrote a small python script to fetch a list of pending accounts, show me the info, and let me pick which ones to approve. It then does that, and follows each account, and toots a welcome message.
It worked well!
Script output listing 3 pending accounts, details redacted [IMG]
But ...
So, I extended it a bit.
Now it also:
Script output listing 3 pending accounts, with the above improvements [IMG]
It's probably not much use to anyone else, but if you remove the callsign and QRZ lookups it might be a good base for your own?
from datetime import datetime, timezone from mastodon import Mastodon from random import choice from qrz import QRZ import humanize import pprint import re callsignRegex = '[a-zA-Z0-9]{1,3}[0-9][a-zA-Z0-9]{0,3}[a-zA-Z]' ruleCanary = 'hippo cornflakes' qrz = QRZ(cfg='./settings.cfg') pp = pprint.PrettyPrinter(indent=4) ''' create app - do this once Mastodon.create_app( 'mastoWelcome', scopes=['admin:read:accounts', 'admin:write:accounts', 'write:statuses', 'write:follows'], api_base_url = 'https://mastodon.radio', to_file = 'mastoWelcome_clientcred.secret' ) ''' ''' authorize - do this once mastodon = Mastodon( client_id = 'mastoWelcome_clientcred.secret', api_base_url = 'https://mastodon.radio' ) ''' ''' log in - do this as often as needed mastodon.log_in( 'username', 'password', scopes=['admin:read:accounts', 'admin:write:accounts', 'write:statuses', 'write:follows'], to_file = 'mastoWelcome_usercred.secret' ) ''' # connect with saved credentials mastodon = Mastodon( access_token = 'mastoWelcome_usercred.secret', api_base_url = 'https://mastodon.radio' ) pendingAccounts = mastodon.admin_accounts(status='pending') print(str(len(pendingAccounts)) + ' Pending accounts') i = 0 for pAccount in pendingAccounts: # easy to type ID print(f'\n{i}') # attempt to find callsigns allPossibleCallsigns = re.findall(callsignRegex, pAccount['username']) allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['email'])) allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['invite_request'])) # tell me about the account print(f"Username: {pAccount['username']}") print(f'{humanize.naturaltime(datetime.now(timezone.utc) - pAccount["created_at"])} {pAccount["email"]}') print(f'{pAccount["invite_request"]}') if ruleCanary in pAccount["invite_request"]: print(f'\033[93mRule Canary: {ruleCanary in pAccount["invite_request"]}\033[0m') else: print(f'\033[95mRule Canary: {ruleCanary in pAccount["invite_request"]}\033[0m') if pAccount["confirmed"]: print(f'\033[93mEmail confirmed: {pAccount["confirmed"]}\033[0m') else: print(f'\033[95mEmail confirmed: {pAccount["confirmed"]}\033[0m') # dedupe list possibleCallsigns = [] for call in allPossibleCallsigns: lowerCall = call.casefold() if lowerCall not in possibleCallsigns: possibleCallsigns.append(lowerCall) print(f'Possible Callsigns: {possibleCallsigns}') # get info from QRZ print('QRZ info') for call in possibleCallsigns: result = qrz.callsign(call) pp.pprint(result) i+=1 # ask me what accounts look OK approveWhat = input("Which accounts to approve?: ") # split the space separated list approveWhat = approveWhat.split() print('\nYou want to approve these account:') for aAccount in approveWhat: print(pendingAccounts[int(aAccount)]['username']) # make a case sensitive YES to make sure I'm paying attention confirmYES = ''.join(map(choice, zip('yes', 'YES'))) confirm = input(f"type '{confirmYES}' to confirm: ") # store the usernames in "@ + name" format userNames = '' if confirm == confirmYES: print('Approving the accounts...') for aAccount in approveWhat: updatedAccount = mastodon.admin_account_approve(pendingAccounts[int(aAccount)]['id']) print(f'Approved {updatedAccount["username"]}!') mastodon.account_follow(pendingAccounts[int(aAccount)]['id']) print('Followed them!') userNames += f'@{updatedAccount["username"]}\n' # YOU'LL WANT TO CHANGE THIS MESSAGE (I assume) mastodon.toot(f'Hello @{updatedAccount["username"]}\nPlease toot an introduction (using # introductions) so we can get to know you :alex_grin: add some hashtags too so we know what interests you\n\nYou can find more people on our server using the local timeline & directory\nhttps://mastodon.radio/web/directory\n\nThis list of radio amateurs on mastodon, may be of interest: https://pad.dc7ia.eu/p/radio_amateurs_on_the_fediverse\n\nI recommend the third-party apps\nhttps://joinmastodon.org/apps\nRemember to describe your images (ask for help!) + fill out your bio before following people', visibility='unlisted') print('Tooting to welcome them!') templateToot = f'Everyone, please welcome\n{userNames}\n\n#introductions' print('Tooting bulk welcome message...') mastodon.toot(templateToot) else: print('\nOk, DOING NOTHING.')
mastodon.radio isn't a big corporate server, we have rules that we enforce. Some are pretty common (don't be a dick) some are important for the community (describe your images) and I see people not describing their images. Which makes me think they haven't read the rules. So I wondered how I could check up on this.
Maybe you've heard of a warrant canary[1]? The idea being a message says that "The FBI has not been here" and is removed if they have been, even if an organisation is forbidden from saying, for example, that the FBI HAS been there.
1: https://en.wikipedia.org/wiki/Warrant_canary
Based on this idea I added "rule 7" to mastodon.radio, it reads
When apply for an account, if you've actually read these rules, please end your "Why do you want to join?" with the phrase "hippo cornflakes"
Very few requests mention cornflakes, or hippos. But that's not really the point, I'm not denying entry to people who don't. Yet. But it is interesting to see, from the administrator's view, who actually read the rules AND bothered to follow them.
November 2022 has seen a lot more applications for accounts than ever before to mastodon.radio, as a result I've made some changes to the script which remove some of the manual checks and speed up the process.
I *could* automate the decision further, specifically if a callsign is found and gets at least one result in QRZ lookup. But I've held off that for now because I'd have to make more significant code changes to move that lookup around, AND I'm not convinced that a bad actor couldn't just pop in a valid callsign and then I'm automating the approval of spam. I know the rule canary could have this issue too, but it's easy to change that and it remain effective (for a time) but the only mitigation to "callsign stuffing" would be to remove the automation again.
For four years I've welcomed everyone personally, when I wrote this script I was able to easily send a direct message to every new account with some tips.
This worked fine, until we had literally hundreds of new users and my mentions and direct messages became impossible to use.
The solution? Make Alex do it! Alex is the mastodon.radio mascot and has had an account for a while which has always been restricted, with no followers, and not posting anything. But I've opened that up and now it's Alex who welcomes every new user.
It adds some complexity to the script because now it has to log in twice, but it makes my life a lot easier and my use of mastodon.radio a LOT nicer. It also means that it doesn't matter who does the approval, everyone gets a consistent welcome and everyone else has one place to look for these welcome messages.
I'm logged in as Alex on my phone so I can spot anyone asking questions to them, and so far it has been very well received - with replies like:
A mascot! Now that's what I'm talking about!
Who's a good bot!? You're a good bot!
Plus now more people get to see our cool mascot! If you want to know about new arrivals on mastodon.radio give Alex[2] a follow.
2: https://mastodon.radio/@alex
This is the newly refined code with more Alex
''' ALL THE SAME STUFF AS BEFORE BUT WITH TWO ACCOUNTS ''' # connect with saved credentials mastodon = Mastodon( access_token = 'mastoWelcome_usercred.secret', api_base_url = 'https://mastodon.radio' ) # THIS IS NEW mastodonMascot = Mastodon( access_token = 'mastoWelcome_usercredMascot.secret', api_base_url = 'https://mastodon.radio' ) pendingAccounts = mastodon.admin_accounts(status='pending') print(str(len(pendingAccounts)) + ' Pending accounts') autoSelected = [] i = 0 for pAccount in pendingAccounts: if pAccount["confirmed"]: # THIS IS NEW if ruleCanary in pAccount["invite_request"].casefold(): autoSelected.append(i) else: print(f'\n{i}') allPossibleCallsigns = re.findall(callsignRegex, pAccount['username']) allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['email'])) allPossibleCallsigns.extend(re.findall(callsignRegex, pAccount['invite_request'])) print(f"\033[93mUsername: {pAccount['username']}\033[0m") print(f'{humanize.naturaltime(datetime.now(timezone.utc) - pAccount["created_at"])} {pAccount["email"]}') print(f'\033[95m{pAccount["invite_request"]}\033[0m') # dedupe list possibleCallsigns = [] for call in allPossibleCallsigns: lowerCall = call.casefold() if lowerCall not in possibleCallsigns: possibleCallsigns.append(lowerCall) print(f'Possible Callsigns: {possibleCallsigns}') # get info from QRZ print('QRZ info') for call in possibleCallsigns: try: result = qrz.callsign(call) pp.pprint(result) except: print(f'{call} not found') i+=1 # THIS IS NEW print(f'Auto Selected {len(autoSelected)} accounts') for aAccount in autoSelected: print(pendingAccounts[int(aAccount)]['username']) approveWhat = input("Which accounts to approve?: ") approveWhat = approveWhat.split() print(f'\nYou want to approve these {len(approveWhat)} accounts:') for aAccount in approveWhat: print(pendingAccounts[int(aAccount)]['username']) # make a case sensitive YES to make sure I'm paying attention confirmYES = ''.join(map(choice, zip('yes', 'YES'))) confirm = input(f"type '{confirmYES}' to confirm: ") userNames = '' if confirm == confirmYES: print('Approving the accounts...') for aAccount in autoSelected + approveWhat: updatedAccount = mastodon.admin_account_approve(pendingAccounts[int(aAccount)]['id']) print(f'Approved {updatedAccount["username"]}!') mastodon.account_follow(pendingAccounts[int(aAccount)]['id']) print('Followed them!') userNames += f'@{updatedAccount["username"]}\n' try: mastodonMascot.status_post(f'Welcome to mastodon.radio @{updatedAccount["username"]}!\nAny questions, your admin is M0YNG\n\nPlease toot an #introduction with topic hashtags so we can get to know you :alex_grin:\n\nYou can find more people on our server using the local timeline\nhttps://mastodon.radio/web/public/local\nPlease fill out your bio before following people.\n\nThis list of radio amateurs on mastodon may be of interest\nhttps://pad.dc7ia.eu/p/radio_amateurs_on_the_fediverse\n\nI recommend the third-party apps\nhttps://joinmastodon.org/apps\n\nRemember to describe your images!', visibility='direct') print('Tooting to welcome them!') except: print(f'Failed to welcome {updatedAccount["username"]}') templateToot = f'Everyone, please welcome\n{userNames}\n\n#introduction' print('Tooting bulk welcome message...') mastodonMascot.toot(templateToot) else: print('\nOk, DOING NOTHING.')
-+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+-
๐ค Black Lives Matter
๐๐ค๐ Trans Rights are Human Rights
โค๏ธ๐งก๐๐๐๐ Love is Love
Copyright ยฉ 2022 Christopher M0YNG
Code snippets are licenced under the Hippocratic License 3.0 (or later.)
Page generated 2022-12-11 by Complex 19