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

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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. group_to_founders = defaultdict(list)
  25. channel_to_founder = defaultdict(lambda: (None, None))
  26. for line in infile:
  27. line = line.rstrip('\r\n')
  28. parts = line.split(' ')
  29. category = parts[0]
  30. if category == 'GACL':
  31. # Note: all group definitions precede channel access entries (token CA) by design, so it
  32. # should be safe to read this in using one pass.
  33. groupname = parts[1]
  34. user = parts[2]
  35. flags = parts[3]
  36. if 'F' in flags:
  37. group_to_founders[groupname].append(user)
  38. elif category == 'MU':
  39. # user account
  40. # MU AAAAAAAAB shivaram $1$hcspif$nCm4r3S14Me9ifsOPGuJT. user@example.com 1600134392 1600467343 +sC default
  41. name = parts[2]
  42. user = {'name': name, 'hash': parts[3], 'email': parts[4], 'registeredAt': to_unixnano(parts[5])}
  43. out['users'][name].update(user)
  44. pass
  45. elif category == 'MN':
  46. # grouped nick
  47. # MN shivaram slingamn 1600218831 1600467343
  48. username, groupednick = parts[1], parts[2]
  49. if username != groupednick:
  50. user = out['users'][username]
  51. user.setdefault('additionalnicks', []).append(groupednick)
  52. elif category == 'MDU':
  53. if parts[2] == 'private:usercloak':
  54. username = parts[1]
  55. out['users'][username]['vhost'] = parts[3]
  56. elif category == 'MC':
  57. # channel registration
  58. # MC #mychannel 1600134478 1600467343 +v 272 0 0
  59. # MC #NEWCHANNELTEST 1602270889 1602270974 +vg 1 0 0 jaeger4
  60. chname = parts[1]
  61. chdata = out['channels'][chname]
  62. # XXX just give everyone +nt, regardless of lock status; they can fix it later
  63. chdata.update({'name': chname, 'registeredAt': to_unixnano(parts[2])})
  64. if parts[8] != '':
  65. chdata['key'] = parts[8]
  66. modes = {'n', 't'}
  67. mlock_on, mlock_off = int(parts[5]), int(parts[6])
  68. for flag, mode in CMODE_FLAG_TO_MODE.items():
  69. if flag & mlock_on != 0:
  70. modes.add(mode)
  71. elif flag & mlock_off != 0 and mode in modes:
  72. modes.remove(mode)
  73. chdata['modes'] = ''.join(sorted(modes))
  74. chdata['limit'] = int(parts[7])
  75. elif category == 'MDC':
  76. # auxiliary data for a channel registration
  77. # MDC #mychannel private:topic:setter s
  78. # MDC #mychannel private:topic:text hi again
  79. # MDC #mychannel private:topic:ts 1600135864
  80. chname = parts[1]
  81. category = parts[2]
  82. if category == 'private:topic:text':
  83. out['channels'][chname]['topic'] = line.split(maxsplit=3)[3]
  84. elif category == 'private:topic:setter':
  85. out['channels'][chname]['topicSetBy'] = parts[3]
  86. elif category == 'private:topic:ts':
  87. out['channels'][chname]['topicSetAt'] = to_unixnano(parts[3])
  88. elif category == 'private:mlockext':
  89. # the channel forward mode is +L on insp/unreal, +f on charybdis
  90. # charybdis has a +L ("large banlist") taking no argument
  91. # and unreal has a +f ("flood limit") taking two colon-delimited numbers,
  92. # so check for an argument that starts with a #
  93. if parts[3].startswith('L#') or parts[3].startswith('f#'):
  94. out['channels'][chname]['forward'] = parts[3][1:]
  95. elif category == 'CA':
  96. # channel access lists
  97. # CA #mychannel shivaram +AFORafhioqrstv 1600134478 shivaram
  98. chname, username, flags, set_at = parts[1], parts[2], parts[3], int(parts[4])
  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. # If the username starts with "!", it's actually a GroupServ group.
  108. if username.startswith('!'):
  109. group_founders = group_to_founders.get(username)
  110. if not group_founders:
  111. # skip this and warn about it later
  112. continue
  113. # attempt to promote the first group founder to channel founder
  114. username = group_founders[0]
  115. # but everyone gets the +q flag
  116. for founder in group_founders:
  117. chdata['amode'][founder] = 'q'
  118. # there can only be one founder
  119. preexisting_founder, preexisting_set_at = channel_to_founder[chname]
  120. if preexisting_founder is None or set_at < preexisting_set_at:
  121. chdata['founder'] = username
  122. channel_to_founder[chname] = (username, set_at)
  123. # but multiple people can receive the 'q' amode
  124. chdata['amode'][username] = 'q'
  125. continue
  126. if MASK_MAGIC_REGEX.search(username):
  127. # ignore groups, masks, etc. for any field other than founder
  128. continue
  129. # record the first appearing successor, if necessary
  130. if 'S' in flags:
  131. if not chdata.get('successor'):
  132. chdata['successor'] = username
  133. # finally, handle amodes
  134. if 'q' in flags:
  135. chdata['amode'][username] = 'q'
  136. elif 'a' in flags:
  137. chdata['amode'][username] = 'a'
  138. elif 'o' in flags or 'O' in flags:
  139. chdata['amode'][username] = 'o'
  140. elif 'h' in flags or 'H' in flags:
  141. chdata['amode'][username] = 'h'
  142. elif 'v' in flags or 'V' in flags:
  143. chdata['amode'][username] = 'v'
  144. else:
  145. pass
  146. # do some basic integrity checks
  147. def validate_user(name):
  148. if not name:
  149. return False
  150. return bool(out['users'].get(name))
  151. invalid_channels = []
  152. for chname, chdata in out['channels'].items():
  153. if not validate_user(chdata.get('founder')):
  154. if validate_user(chdata.get('successor')):
  155. chdata['founder'] = chdata['successor']
  156. else:
  157. invalid_channels.append(chname)
  158. for chname in invalid_channels:
  159. logging.warning("Unable to find a valid founder for channel %s, discarding it", chname)
  160. del out['channels'][chname]
  161. return out
  162. def main():
  163. if len(sys.argv) != 3:
  164. raise Exception("Usage: atheme2json.py atheme_db output.json")
  165. with open(sys.argv[1]) as infile:
  166. output = convert(infile)
  167. with open(sys.argv[2], 'w') as outfile:
  168. json.dump(output, outfile)
  169. if __name__ == '__main__':
  170. logging.basicConfig()
  171. sys.exit(main())