-- Leo's gemini proxy

-- Connecting to m0yng.uk:1965...

-- Connected

-- Sending request

-- Meta line: 20 text/gemini;lang=en

mastoWelcomer - M0YNG.uk

Created 2022-05-22

Modified 2022-11-18

Tagged

Mastodon

Hacking

Code


Page Content


[TOC]


Background


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:


look at pending accounts

confirm email is confirmed (not visible on approval page!)

confirm I'm happy with reason

maybe check the callsign too

select for approval

copy usernames (using snippet in dev console)

approve

copy old welcome toot

paste usernames into welcome toot

send toot

find accounts again

follow the new accounts


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!


Proof of concept


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 ...


It was hard to use on my phone (copying long ID strings)

It wasn't obvious that the email was confirmed

The date time thing is too long

It didn't check if the resulting welcome toot is too long, so it could fail to send


Current version


So, I extended it a bit.


Now it also:


formats the relative date/time

tries to find callsigns in the username, email, and request text

looks for my "rules canary" (more below)

highlights if email is confirmed

checks QRZ.com for any found callsigns

lets me type single digits to select accounts!

sends individual welcome messages, plus a public welcome


Script output listing 3 pending accounts, with the above improvements [IMG]


The Code


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?


**Note** I've kept the permissions as minimal as possible, but they are still ADMIN tokens lying around on your computer. Due caution is required.


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.')

Rules Canary?


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.


Some workflow optimisations [November 2022]


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.


Only show confirmed emails - before I had to eyeball which accounts had confirmed their email address, now it only shows me accounts with a confirmed email address.

Rule Canary - the script now creates an "Auto Selected" list of accounts which have confirmed their email AND have included the rule canary text (plus I made it not case sensitive!)

Stop showing if email is confirmed and rule canary present, as we're automatically deciding so don't need to see them

Added some try/except statements to avoid the entire script failing part way through


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.


Make Alex do it [November 2022]


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


The updated code [November 2022]


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 ยฉ 2024 Christopher M0YNG - It is forbidden to use any part of this site for crypto/NFT/AI related projects.

Code snippets are licenced under the Hippocratic License 3.0 (or later.)

Page generated 2024-03-24 by Complex 19

-- Response ended

-- Page fetched on Sat May 18 20:44:39 2024