You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

atheme2json.py 7.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/usr/bin/python3
  2. import json
  3. import logging
  4. import re
  5. import sys
  6. from collections import defaultdict
  7. MASK_MAGIC_REGEX = re.compile(r'[*?!@]')
  8. def to_unixnano(timestamp):
  9. return int(timestamp) * (10**9)
  10. # include/atheme/channels.h
  11. CMODE_FLAG_TO_MODE = {
  12. 0x001: 'i', # CMODE_INVITE
  13. 0x010: 'n', # CMODE_NOEXT
  14. 0x080: 's', # CMODE_SEC
  15. 0x100: 't', # CMODE_TOPIC
  16. }
  17. def convert(infile):
  18. out = {
  19. 'version': 1,
  20. 'source': 'atheme',
  21. 'users': defaultdict(dict),
  22. 'channels': defaultdict(dict),
  23. }
  24. # Translate channels owned by groups to being owned by the first founder of that group
  25. # Otherwise the code crashes on networks using atheme's GroupServ
  26. # Note: all group definitions precede channel access entries (token CA) by design, so it
  27. # should be safe to read this in using one pass.
  28. groups_to_user = {}
  29. channel_to_founder = defaultdict(lambda: (None, None))
  30. for line in infile:
  31. line = line.rstrip('\r\n')
  32. parts = line.split(' ')
  33. category = parts[0]
  34. if category == 'GACL':
  35. groupname = parts[1]
  36. user = parts[2]
  37. flags = parts[3]
  38. # Pick the first founder
  39. if groupname not in groups_to_user and 'F' in flags:
  40. groups_to_user[groupname] = user
  41. if category == 'MU':
  42. # user account
  43. # MU AAAAAAAAB shivaram $1$hcspif$nCm4r3S14Me9ifsOPGuJT. user@example.com 1600134392 1600467343 +sC default
  44. name = parts[2]
  45. user = {'name': name, 'hash': parts[3], 'email': parts[4], 'registeredAt': to_unixnano(parts[5])}
  46. out['users'][name].update(user)
  47. pass
  48. elif category == 'MN':
  49. # grouped nick
  50. # MN shivaram slingamn 1600218831 1600467343
  51. username, groupednick = parts[1], parts[2]
  52. if username != groupednick:
  53. user = out['users'][username]
  54. if 'additionalNicks' not in user:
  55. user['additionalNicks'] = []
  56. user['additionalNicks'].append(groupednick)
  57. elif category == 'MDU':
  58. if parts[2] == 'private:usercloak':
  59. username = parts[1]
  60. out['users'][username]['vhost'] = parts[3]
  61. elif category == 'MC':
  62. # channel registration
  63. # MC #mychannel 1600134478 1600467343 +v 272 0 0
  64. # MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4
  65. chname = parts[1]
  66. chdata = out['channels'][chname]
  67. # XXX just give everyone +nt, regardless of lock status; they can fix it later
  68. chdata.update({'name': chname, 'registeredAt': to_unixnano(parts[2])})
  69. if parts[8] != '':
  70. chdata['key'] = parts[8]
  71. modes = {'n', 't'}
  72. mlock_on, mlock_off = int(parts[5]), int(parts[6])
  73. for flag, mode in CMODE_FLAG_TO_MODE.items():
  74. if flag & mlock_on != 0:
  75. modes.add(mode)
  76. elif flag & mlock_off != 0 and mode in modes:
  77. modes.remove(mode)
  78. chdata['modes'] = ''.join(sorted(modes))
  79. chdata['limit'] = int(parts[7])
  80. elif category == 'MDC':
  81. # auxiliary data for a channel registration
  82. # MDC #mychannel private:topic:setter s
  83. # MDC #mychannel private:topic:text hi again
  84. # MDC #mychannel private:topic:ts 1600135864
  85. chname = parts[1]
  86. category = parts[2]
  87. if category == 'private:topic:text':
  88. out['channels'][chname]['topic'] = line.split(maxsplit=3)[3]
  89. elif category == 'private:topic:setter':
  90. out['channels'][chname]['topicSetBy'] = parts[3]
  91. elif category == 'private:topic:ts':
  92. out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3])
  93. elif category == 'CA':
  94. # channel access lists
  95. # CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram
  96. chname, username, flags, set_at = parts[1], parts[2], parts[3], int(parts[4])
  97. if MASK_MAGIC_REGEX.search(username):
  98. continue
  99. chname = parts[1]
  100. chdata = out['channels'][chname]
  101. flags = parts[3]
  102. set_at = int(parts[4])
  103. if 'amode' not in chdata:
  104. chdata['amode'] = {}
  105. # see libathemecore/flags.c: +o is op, +O is autoop, etc.
  106. if 'F' in flags:
  107. # there can only be one founder
  108. preexisting_founder, preexisting_set_at = channel_to_founder[chname]
  109. # If the username starts with "!", it's actually a GroupServ group.
  110. if username.startswith('!'):
  111. try:
  112. group_founder = groups_to_user[username]
  113. print(f"WARNING: flattening GroupServ group founder {username} on {chname} to first group founder {group_founder}")
  114. except KeyError:
  115. raise ValueError(f"Got channel {chname} owned by group {username} that has no founder?")
  116. else:
  117. username = group_founder
  118. if preexisting_founder is None or set_at < preexisting_set_at:
  119. chdata['founder'] = username
  120. channel_to_founder[chname] = (username, set_at)
  121. # but multiple people can receive the 'q' amode
  122. chdata['amode'][username] = 'q'
  123. elif 'q' in flags:
  124. chdata['amode'][username] = 'q'
  125. elif 'a' in flags:
  126. chdata['amode'][username] = 'a'
  127. elif 'o' in flags or 'O' in flags:
  128. chdata['amode'][username] = 'o'
  129. elif 'h' in flags or 'H' in flags:
  130. chdata['amode'][username] = 'h'
  131. elif 'v' in flags or 'V' in flags:
  132. chdata['amode'][username] = 'v'
  133. elif 'S' in flags:
  134. # take the first entry as the successor
  135. if not chdata.get('successor'):
  136. chdata['successor'] = username
  137. else:
  138. pass
  139. # do some basic integrity checks
  140. def validate_user(name):
  141. if not name:
  142. return False
  143. return bool(out['users'].get(name))
  144. invalid_channels = []
  145. for chname, chdata in out['channels'].items():
  146. if not validate_user(chdata.get('founder')):
  147. if validate_user(chdata.get('successor')):
  148. chdata['founder'] = chdata['successor']
  149. else:
  150. invalid_channels.append(chname)
  151. for chname in invalid_channels:
  152. logging.warning("Unable to find a valid founder for channel %s, discarding it", chname)
  153. del out['channels'][chname]
  154. return out
  155. def main():
  156. if len(sys.argv) != 3:
  157. raise Exception("Usage: atheme2json.py atheme_db output.json")
  158. with open(sys.argv[1]) as infile:
  159. output = convert(infile)
  160. with open(sys.argv[2], 'w') as outfile:
  161. json.dump(output, outfile)
  162. if __name__ == '__main__':
  163. logging.basicConfig()
  164. sys.exit(main())