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 8.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. #!/usr/bin/python3
  2. import binascii
  3. import json
  4. import logging
  5. import re
  6. import sys
  7. from collections import defaultdict
  8. MASK_MAGIC_REGEX = re.compile(r'[*?!@$]')
  9. def to_unixnano(timestamp):
  10. return int(timestamp) * (10**9)
  11. # include/atheme/channels.h
  12. CMODE_FLAG_TO_MODE = {
  13. 0x001: 'i', # CMODE_INVITE
  14. 0x010: 'n', # CMODE_NOEXT
  15. 0x080: 's', # CMODE_SEC
  16. 0x100: 't', # CMODE_TOPIC
  17. }
  18. # attempt to interpret certfp as a hex-encoded SHA-256 fingerprint
  19. def validate_certfp(certfp):
  20. try:
  21. dec = binascii.unhexlify(certfp)
  22. except:
  23. return False
  24. return len(dec) == 32
  25. def convert(infile):
  26. out = {
  27. 'version': 1,
  28. 'source': 'atheme',
  29. 'users': defaultdict(dict),
  30. 'channels': defaultdict(dict),
  31. }
  32. group_to_founders = defaultdict(list)
  33. channel_to_founder = defaultdict(lambda: (None, None))
  34. while True:
  35. line = infile.readline()
  36. if not line:
  37. break
  38. line = line.rstrip(b'\r\n')
  39. try:
  40. line = line.decode('utf-8')
  41. except UnicodeDecodeError:
  42. line = line.decode('utf-8', 'replace')
  43. logging.warning("line contained invalid utf8 data " + line)
  44. parts = line.split(' ')
  45. category = parts[0]
  46. if category == 'GACL':
  47. # Note: all group definitions precede channel access entries (token CA) by design, so it
  48. # should be safe to read this in using one pass.
  49. groupname = parts[1]
  50. user = parts[2]
  51. flags = parts[3]
  52. if 'F' in flags:
  53. group_to_founders[groupname].append(user)
  54. elif category == 'MU':
  55. # user account
  56. # MU AAAAAAAAB shivaram $1$hcspif$nCm4r3S14Me9ifsOPGuJT. user@example.com 1600134392 1600467343 +sC default
  57. name = parts[2]
  58. user = {'name': name, 'hash': parts[3], 'email': parts[4], 'registeredAt': to_unixnano(parts[5])}
  59. out['users'][name].update(user)
  60. pass
  61. elif category == 'MN':
  62. # grouped nick
  63. # MN shivaram slingamn 1600218831 1600467343
  64. username, groupednick = parts[1], parts[2]
  65. if username != groupednick:
  66. user = out['users'][username]
  67. user.setdefault('additionalnicks', []).append(groupednick)
  68. elif category == 'MDU':
  69. if parts[2] == 'private:usercloak':
  70. username = parts[1]
  71. out['users'][username]['vhost'] = parts[3]
  72. elif category == 'MCFP':
  73. username, certfp = parts[1], parts[2]
  74. if validate_certfp(certfp):
  75. user = out['users'][username]
  76. user.setdefault('certfps', []).append(certfp.lower())
  77. elif category == 'MC':
  78. # channel registration
  79. # MC #mychannel 1600134478 1600467343 +v 272 0 0
  80. # MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4
  81. chname = parts[1]
  82. chdata = out['channels'][chname]
  83. # XXX just give everyone +nt, regardless of lock status; they can fix it later
  84. chdata.update({'name': chname, 'registeredAt': to_unixnano(parts[2])})
  85. if parts[8] != '':
  86. chdata['key'] = parts[8]
  87. modes = {'n', 't'}
  88. mlock_on, mlock_off = int(parts[5]), int(parts[6])
  89. for flag, mode in CMODE_FLAG_TO_MODE.items():
  90. if flag & mlock_on != 0:
  91. modes.add(mode)
  92. elif flag & mlock_off != 0 and mode in modes:
  93. modes.remove(mode)
  94. chdata['modes'] = ''.join(sorted(modes))
  95. chdata['limit'] = int(parts[7])
  96. elif category == 'MDC':
  97. # auxiliary data for a channel registration
  98. # MDC #mychannel private:topic:setter s
  99. # MDC #mychannel private:topic:text hi again
  100. # MDC #mychannel private:topic:ts 1600135864
  101. chname = parts[1]
  102. category = parts[2]
  103. if category == 'private:topic:text':
  104. out['channels'][chname]['topic'] = line.split(maxsplit=3)[3]
  105. elif category == 'private:topic:setter':
  106. out['channels'][chname]['topicSetBy'] = parts[3]
  107. elif category == 'private:topic:ts':
  108. out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3])
  109. elif category == 'private:mlockext':
  110. # the channel forward mode is +L on insp/unreal, +f on charybdis
  111. # charybdis has a +L ("large banlist") taking no argument
  112. # and unreal has a +f ("flood limit") taking two colon-delimited numbers,
  113. # so check for an argument that starts with a #
  114. if parts[3].startswith('L#') or parts[3].startswith('f#'):
  115. out['channels'][chname]['forward'] = parts[3][1:]
  116. elif category == 'CA':
  117. # channel access lists
  118. # CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram
  119. chname, username, flags, set_at = parts[1], parts[2], parts[3], int(parts[4])
  120. chname = parts[1]
  121. chdata = out['channels'][chname]
  122. flags = parts[3]
  123. set_at = int(parts[4])
  124. if 'amode' not in chdata:
  125. chdata['amode'] = {}
  126. # see libathemecore/flags.c: +o is op, +O is autoop, etc.
  127. if 'F' in flags:
  128. # If the username starts with "!", it's actually a GroupServ group.
  129. if username.startswith('!'):
  130. group_founders = group_to_founders.get(username)
  131. if not group_founders:
  132. # skip this and warn about it later
  133. continue
  134. # attempt to promote the first group founder to channel founder
  135. username = group_founders[0]
  136. # but everyone gets the +q flag
  137. for founder in group_founders:
  138. chdata['amode'][founder] = 'q'
  139. # there can only be one founder
  140. preexisting_founder, preexisting_set_at = channel_to_founder[chname]
  141. if preexisting_founder is None or set_at < preexisting_set_at:
  142. chdata['founder'] = username
  143. channel_to_founder[chname] = (username, set_at)
  144. # but multiple people can receive the 'q' amode
  145. chdata['amode'][username] = 'q'
  146. continue
  147. if MASK_MAGIC_REGEX.search(username):
  148. # ignore groups, masks, etc. for any field other than founder
  149. continue
  150. # record the first appearing successor, if necessary
  151. if 'S' in flags:
  152. if not chdata.get('successor'):
  153. chdata['successor'] = username
  154. # finally, handle amodes
  155. if 'q' in flags:
  156. chdata['amode'][username] = 'q'
  157. elif 'a' in flags:
  158. chdata['amode'][username] = 'a'
  159. elif 'o' in flags or 'O' in flags:
  160. chdata['amode'][username] = 'o'
  161. elif 'h' in flags or 'H' in flags:
  162. chdata['amode'][username] = 'h'
  163. elif 'v' in flags or 'V' in flags:
  164. chdata['amode'][username] = 'v'
  165. else:
  166. pass
  167. # do some basic integrity checks
  168. def validate_user(name):
  169. if not name:
  170. return False
  171. return bool(out['users'].get(name))
  172. invalid_channels = []
  173. for chname, chdata in out['channels'].items():
  174. if not validate_user(chdata.get('founder')):
  175. if validate_user(chdata.get('successor')):
  176. chdata['founder'] = chdata['successor']
  177. else:
  178. invalid_channels.append(chname)
  179. for chname in invalid_channels:
  180. logging.warning("Unable to find a valid founder for channel %s, discarding it", chname)
  181. del out['channels'][chname]
  182. return out
  183. def main():
  184. if len(sys.argv) != 3:
  185. raise Exception("Usage: atheme2json.py atheme_db output.json")
  186. with open(sys.argv[1], 'rb') as infile:
  187. output = convert(infile)
  188. with open(sys.argv[2], 'w') as outfile:
  189. json.dump(output, outfile)
  190. if __name__ == '__main__':
  191. logging.basicConfig()
  192. sys.exit(main())