๐Ÿ’พ Archived View for m0yng.uk โ€บ 2022 โ€บ 05 โ€บ mastoWelcomer captured on 2023-04-19 at 22:35:08. Gemini links have been rewritten to link to archived content

View Raw

More Information

โฌ…๏ธ Previous capture (2023-01-29)

โžก๏ธ Next capture (2023-05-24)

-=-=-=-=-=-=-

mastoWelcomer - M0YNG.uk

Created 2022-05-22

Modified 2022-11-18

Tagged

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:

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

Current version

So, I extended it a bit.

Now it also:

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?

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.

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 ยฉ 2022 Christopher M0YNG

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

Page generated 2022-12-11 by Complex 19