2020-10-12 21:06:17 +02:00
|
|
|
#!/usr/bin/python3
|
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
import json
|
|
|
|
import logging
|
2020-12-07 02:07:45 +01:00
|
|
|
import re
|
2020-10-02 22:48:37 +02:00
|
|
|
import sys
|
|
|
|
from collections import defaultdict
|
|
|
|
|
2020-12-07 08:53:45 +01:00
|
|
|
MASK_MAGIC_REGEX = re.compile(r'[*?!@$]')
|
2020-12-07 02:07:45 +01:00
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
def to_unixnano(timestamp):
|
|
|
|
return int(timestamp) * (10**9)
|
|
|
|
|
2020-10-12 21:06:17 +02:00
|
|
|
# include/atheme/channels.h
|
|
|
|
CMODE_FLAG_TO_MODE = {
|
|
|
|
0x001: 'i', # CMODE_INVITE
|
|
|
|
0x010: 'n', # CMODE_NOEXT
|
|
|
|
0x080: 's', # CMODE_SEC
|
|
|
|
0x100: 't', # CMODE_TOPIC
|
|
|
|
}
|
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
def convert(infile):
|
|
|
|
out = {
|
|
|
|
'version': 1,
|
|
|
|
'source': 'atheme',
|
|
|
|
'users': defaultdict(dict),
|
|
|
|
'channels': defaultdict(dict),
|
|
|
|
}
|
|
|
|
|
2020-12-07 23:26:31 +01:00
|
|
|
group_to_founders = defaultdict(list)
|
2020-12-04 09:58:01 +01:00
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
channel_to_founder = defaultdict(lambda: (None, None))
|
|
|
|
|
|
|
|
for line in infile:
|
2020-10-12 21:06:17 +02:00
|
|
|
line = line.rstrip('\r\n')
|
|
|
|
parts = line.split(' ')
|
2020-10-02 22:48:37 +02:00
|
|
|
category = parts[0]
|
2020-12-07 23:26:31 +01:00
|
|
|
|
2020-12-04 09:58:01 +01:00
|
|
|
if category == 'GACL':
|
2020-12-07 23:26:31 +01:00
|
|
|
# Note: all group definitions precede channel access entries (token CA) by design, so it
|
|
|
|
# should be safe to read this in using one pass.
|
2020-12-04 09:58:01 +01:00
|
|
|
groupname = parts[1]
|
|
|
|
user = parts[2]
|
|
|
|
flags = parts[3]
|
2020-12-07 23:26:31 +01:00
|
|
|
if 'F' in flags:
|
|
|
|
group_to_founders[groupname].append(user)
|
|
|
|
elif category == 'MU':
|
2020-10-02 22:48:37 +02:00
|
|
|
# user account
|
|
|
|
# MU AAAAAAAAB shivaram $1$hcspif$nCm4r3S14Me9ifsOPGuJT. user@example.com 1600134392 1600467343 +sC default
|
|
|
|
name = parts[2]
|
|
|
|
user = {'name': name, 'hash': parts[3], 'email': parts[4], 'registeredAt': to_unixnano(parts[5])}
|
|
|
|
out['users'][name].update(user)
|
|
|
|
pass
|
|
|
|
elif category == 'MN':
|
|
|
|
# grouped nick
|
|
|
|
# MN shivaram slingamn 1600218831 1600467343
|
|
|
|
username, groupednick = parts[1], parts[2]
|
|
|
|
if username != groupednick:
|
|
|
|
user = out['users'][username]
|
2020-12-07 23:26:31 +01:00
|
|
|
user.setdefault('additionalnicks', []).append(groupednick)
|
2020-10-02 22:48:37 +02:00
|
|
|
elif category == 'MDU':
|
|
|
|
if parts[2] == 'private:usercloak':
|
|
|
|
username = parts[1]
|
|
|
|
out['users'][username]['vhost'] = parts[3]
|
|
|
|
elif category == 'MC':
|
|
|
|
# channel registration
|
|
|
|
# MC #mychannel 1600134478 1600467343 +v 272 0 0
|
2020-10-12 21:06:17 +02:00
|
|
|
# MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4
|
2020-10-02 22:48:37 +02:00
|
|
|
chname = parts[1]
|
2020-10-12 21:06:17 +02:00
|
|
|
chdata = out['channels'][chname]
|
|
|
|
# XXX just give everyone +nt, regardless of lock status; they can fix it later
|
|
|
|
chdata.update({'name': chname, 'registeredAt': to_unixnano(parts[2])})
|
|
|
|
if parts[8] != '':
|
|
|
|
chdata['key'] = parts[8]
|
|
|
|
modes = {'n', 't'}
|
|
|
|
mlock_on, mlock_off = int(parts[5]), int(parts[6])
|
|
|
|
for flag, mode in CMODE_FLAG_TO_MODE.items():
|
|
|
|
if flag & mlock_on != 0:
|
|
|
|
modes.add(mode)
|
2020-11-27 17:25:22 +01:00
|
|
|
elif flag & mlock_off != 0 and mode in modes:
|
2020-10-12 21:06:17 +02:00
|
|
|
modes.remove(mode)
|
2020-11-27 20:20:58 +01:00
|
|
|
chdata['modes'] = ''.join(sorted(modes))
|
2020-10-12 21:06:17 +02:00
|
|
|
chdata['limit'] = int(parts[7])
|
2020-10-02 22:48:37 +02:00
|
|
|
elif category == 'MDC':
|
|
|
|
# auxiliary data for a channel registration
|
|
|
|
# MDC #mychannel private:topic:setter s
|
|
|
|
# MDC #mychannel private:topic:text hi again
|
|
|
|
# MDC #mychannel private:topic:ts 1600135864
|
|
|
|
chname = parts[1]
|
|
|
|
category = parts[2]
|
|
|
|
if category == 'private:topic:text':
|
2020-12-06 18:38:18 +01:00
|
|
|
out['channels'][chname]['topic'] = line.split(maxsplit=3)[3]
|
2020-10-02 22:48:37 +02:00
|
|
|
elif category == 'private:topic:setter':
|
|
|
|
out['channels'][chname]['topicSetBy'] = parts[3]
|
|
|
|
elif category == 'private:topic:ts':
|
|
|
|
out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3])
|
2020-12-14 11:00:21 +01:00
|
|
|
elif category == 'private:mlockext':
|
|
|
|
# the channel forward mode is +L on insp/unreal, +f on charybdis
|
|
|
|
# charybdis has a +L ("large banlist") taking no argument
|
|
|
|
# and unreal has a +f ("flood limit") taking two colon-delimited numbers,
|
|
|
|
# so check for an argument that starts with a #
|
|
|
|
if parts[3].startswith('L#') or parts[3].startswith('f#'):
|
|
|
|
out['channels'][chname]['forward'] = parts[3][1:]
|
2020-10-02 22:48:37 +02:00
|
|
|
elif category == 'CA':
|
|
|
|
# channel access lists
|
|
|
|
# CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram
|
|
|
|
chname, username, flags, set_at = parts[1], parts[2], parts[3], int(parts[4])
|
|
|
|
chname = parts[1]
|
|
|
|
chdata = out['channels'][chname]
|
|
|
|
flags = parts[3]
|
|
|
|
set_at = int(parts[4])
|
|
|
|
if 'amode' not in chdata:
|
|
|
|
chdata['amode'] = {}
|
2020-10-12 21:06:17 +02:00
|
|
|
# see libathemecore/flags.c: +o is op, +O is autoop, etc.
|
2020-10-05 17:44:22 +02:00
|
|
|
if 'F' in flags:
|
2020-12-04 09:58:01 +01:00
|
|
|
# If the username starts with "!", it's actually a GroupServ group.
|
|
|
|
if username.startswith('!'):
|
2020-12-07 23:26:31 +01:00
|
|
|
group_founders = group_to_founders.get(username)
|
|
|
|
if not group_founders:
|
|
|
|
# skip this and warn about it later
|
|
|
|
continue
|
|
|
|
# attempt to promote the first group founder to channel founder
|
|
|
|
username = group_founders[0]
|
|
|
|
# but everyone gets the +q flag
|
|
|
|
for founder in group_founders:
|
|
|
|
chdata['amode'][founder] = 'q'
|
|
|
|
# there can only be one founder
|
|
|
|
preexisting_founder, preexisting_set_at = channel_to_founder[chname]
|
2020-10-02 22:48:37 +02:00
|
|
|
if preexisting_founder is None or set_at < preexisting_set_at:
|
|
|
|
chdata['founder'] = username
|
|
|
|
channel_to_founder[chname] = (username, set_at)
|
|
|
|
# but multiple people can receive the 'q' amode
|
2020-10-12 21:06:17 +02:00
|
|
|
chdata['amode'][username] = 'q'
|
2020-12-07 08:53:45 +01:00
|
|
|
continue
|
|
|
|
if MASK_MAGIC_REGEX.search(username):
|
|
|
|
# ignore groups, masks, etc. for any field other than founder
|
|
|
|
continue
|
|
|
|
# record the first appearing successor, if necessary
|
|
|
|
if 'S' in flags:
|
|
|
|
if not chdata.get('successor'):
|
|
|
|
chdata['successor'] = username
|
|
|
|
# finally, handle amodes
|
|
|
|
if 'q' in flags:
|
2020-10-12 21:06:17 +02:00
|
|
|
chdata['amode'][username] = 'q'
|
2020-12-04 10:00:28 +01:00
|
|
|
elif 'a' in flags:
|
|
|
|
chdata['amode'][username] = 'a'
|
2020-10-12 21:06:17 +02:00
|
|
|
elif 'o' in flags or 'O' in flags:
|
|
|
|
chdata['amode'][username] = 'o'
|
|
|
|
elif 'h' in flags or 'H' in flags:
|
|
|
|
chdata['amode'][username] = 'h'
|
|
|
|
elif 'v' in flags or 'V' in flags:
|
|
|
|
chdata['amode'][username] = 'v'
|
2020-10-02 22:48:37 +02:00
|
|
|
else:
|
|
|
|
pass
|
|
|
|
|
|
|
|
# do some basic integrity checks
|
2020-12-07 02:50:06 +01:00
|
|
|
def validate_user(name):
|
|
|
|
if not name:
|
|
|
|
return False
|
|
|
|
return bool(out['users'].get(name))
|
|
|
|
|
|
|
|
invalid_channels = []
|
|
|
|
|
2020-10-02 22:48:37 +02:00
|
|
|
for chname, chdata in out['channels'].items():
|
2020-12-07 02:50:06 +01:00
|
|
|
if not validate_user(chdata.get('founder')):
|
|
|
|
if validate_user(chdata.get('successor')):
|
|
|
|
chdata['founder'] = chdata['successor']
|
|
|
|
else:
|
|
|
|
invalid_channels.append(chname)
|
|
|
|
|
|
|
|
for chname in invalid_channels:
|
|
|
|
logging.warning("Unable to find a valid founder for channel %s, discarding it", chname)
|
|
|
|
del out['channels'][chname]
|
2020-10-02 22:48:37 +02:00
|
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
def main():
|
|
|
|
if len(sys.argv) != 3:
|
|
|
|
raise Exception("Usage: atheme2json.py atheme_db output.json")
|
|
|
|
with open(sys.argv[1]) as infile:
|
|
|
|
output = convert(infile)
|
|
|
|
with open(sys.argv[2], 'w') as outfile:
|
|
|
|
json.dump(output, outfile)
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
logging.basicConfig()
|
|
|
|
sys.exit(main())
|