capabilities.py 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262
  1. __filename__ = "capabilities.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.0.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. import os
  9. import datetime
  10. import time
  11. import json
  12. import commentjson
  13. from auth import createPassword
  14. from utils import getNicknameFromActor
  15. from utils import getDomainFromActor
  16. def getOcapFilename(baseDir :str,nickname: str,domain: str,actor :str,subdir: str) -> str:
  17. """Returns the filename for a particular capability accepted or granted
  18. Also creates directories as needed
  19. """
  20. if not actor:
  21. return None
  22. if ':' in domain:
  23. domain=domain.split(':')[0]
  24. if not os.path.isdir(baseDir+'/accounts'):
  25. os.mkdir(baseDir+'/accounts')
  26. ocDir=baseDir+'/accounts/'+nickname+'@'+domain
  27. if not os.path.isdir(ocDir):
  28. os.mkdir(ocDir)
  29. ocDir=baseDir+'/accounts/'+nickname+'@'+domain+'/ocap'
  30. if not os.path.isdir(ocDir):
  31. os.mkdir(ocDir)
  32. ocDir=baseDir+'/accounts/'+nickname+'@'+domain+'/ocap/'+subdir
  33. if not os.path.isdir(ocDir):
  34. os.mkdir(ocDir)
  35. return baseDir+'/accounts/'+nickname+'@'+domain+'/ocap/'+subdir+'/'+actor.replace('/','#')+'.json'
  36. def CapablePost(postJson: {}, capabilityList: [], debug :bool) -> bool:
  37. """Determines whether a post arriving in the inbox
  38. should be accepted accoring to the list of capabilities
  39. """
  40. if postJson.get('type'):
  41. # No announces/repeats
  42. if postJson['type']=='Announce':
  43. if 'inbox:noannounce' in capabilityList:
  44. if debug:
  45. print('DEBUG: inbox post rejected because inbox:noannounce')
  46. return False
  47. # No likes
  48. if postJson['type']=='Like':
  49. if 'inbox:nolike' in capabilityList:
  50. if debug:
  51. print('DEBUG: inbox post rejected because inbox:nolike')
  52. return False
  53. if postJson['type']=='Create':
  54. if postJson.get('object'):
  55. # Does this have a reply?
  56. if postJson['object'].get('inReplyTo'):
  57. if postJson['object']['inReplyTo']:
  58. if 'inbox:noreply' in capabilityList:
  59. if debug:
  60. print('DEBUG: inbox post rejected because inbox:noreply')
  61. return False
  62. # are content warnings enforced?
  63. if postJson['object'].get('sensitive'):
  64. if not postJson['object']['sensitive']:
  65. if 'inbox:cw' in capabilityList:
  66. if debug:
  67. print('DEBUG: inbox post rejected because inbox:cw')
  68. return False
  69. # content warning must have non-zero summary
  70. if postJson['object'].get('summary'):
  71. if len(postJson['object']['summary'])<2:
  72. if 'inbox:cw' in capabilityList:
  73. if debug:
  74. print('DEBUG: inbox post rejected because inbox:cw, summary missing')
  75. return False
  76. if 'inbox:write' in capabilityList:
  77. return True
  78. return True
  79. def capabilitiesRequest(baseDir: str,httpPrefix: str,domain: str, \
  80. requestedActor: str, \
  81. requestedCaps=["inbox:write","objects:read"]) -> {}:
  82. # This is sent to the capabilities endpoint /caps/new
  83. # which could be instance wide or for a particular person
  84. # This could also be added to a follow activity
  85. ocapId=createPassword(32)
  86. ocapRequest = {
  87. "@context": "https://www.w3.org/ns/activitystreams",
  88. "id": httpPrefix+"://"+requestedDomain+"/caps/request/"+ocapId,
  89. "type": "Request",
  90. "capability": requestedCaps,
  91. "actor": requestedActor
  92. }
  93. return ocapRequest
  94. def capabilitiesAccept(baseDir: str,httpPrefix: str, \
  95. nickname: str,domain: str, port: int, \
  96. acceptedActor: str, saveToFile: bool, \
  97. acceptedCaps=["inbox:write","objects:read"]) -> {}:
  98. # This gets returned to capabilities requester
  99. # This could also be added to a follow Accept activity
  100. # reject excessively long actors
  101. if len(acceptedActor)>256:
  102. return None
  103. fullDomain=domain
  104. if port:
  105. if port!=80 and port !=443:
  106. if ':' not in domain:
  107. fullDomain=domain+':'+str(port)
  108. # make directories to store capabilities
  109. ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,acceptedActor,'accept')
  110. if not ocapFilename:
  111. return None
  112. ocapAccept=None
  113. # if the capability already exists then load it from file
  114. if os.path.isfile(ocapFilename):
  115. with open(ocapFilename, 'r') as fp:
  116. ocapAccept=commentjson.load(fp)
  117. # otherwise create a new capability
  118. if not ocapAccept:
  119. acceptedActorNickname=getNicknameFromActor(acceptedActor)
  120. if not acceptedActorNickname:
  121. print('WARN: unable to find nickname in '+acceptedActor)
  122. return None
  123. acceptedActorDomain,acceptedActorPort=getDomainFromActor(acceptedActor)
  124. if acceptedActorPort:
  125. ocapId=acceptedActorNickname+'@'+acceptedActorDomain+':'+str(acceptedActorPort)+'#'+createPassword(32)
  126. else:
  127. ocapId=acceptedActorNickname+'@'+acceptedActorDomain+'#'+createPassword(32)
  128. ocapAccept = {
  129. "@context": "https://www.w3.org/ns/activitystreams",
  130. "id": httpPrefix+"://"+fullDomain+"/caps/"+ocapId,
  131. "type": "Capability",
  132. "capability": acceptedCaps,
  133. "scope": acceptedActor,
  134. "actor": httpPrefix+"://"+fullDomain
  135. }
  136. if nickname:
  137. ocapAccept['actor']=httpPrefix+"://"+fullDomain+'/users/'+nickname
  138. if saveToFile:
  139. with open(ocapFilename, 'w') as fp:
  140. commentjson.dump(ocapAccept, fp, indent=4, sort_keys=False)
  141. return ocapAccept
  142. def capabilitiesGrantedSave(baseDir :str,nickname :str,domain :str,ocap: {}) -> bool:
  143. """A capabilities accept is received, so stor it for
  144. reference when sending to the actor
  145. """
  146. if not ocap.get('actor'):
  147. return False
  148. ocapFilename=getOcapFilename(baseDir,nickname,domain,ocap['actor'],'granted')
  149. if not ocapFilename:
  150. return False
  151. with open(ocapFilename, 'w') as fp:
  152. commentjson.dump(ocap, fp, indent=4, sort_keys=False)
  153. return True
  154. def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
  155. nickname: str,domain: str, port: int, \
  156. updateActor: str, \
  157. updateCaps: []) -> {}:
  158. """Used to sends an update for a change of object capabilities
  159. Note that the capability id gets changed with a new random token
  160. so that the old capabilities can't continue to be used
  161. """
  162. # reject excessively long actors
  163. if len(updateActor)>256:
  164. return None
  165. fullDomain=domain
  166. if port:
  167. if port!=80 and port !=443:
  168. if ':' not in domain:
  169. fullDomain=domain+':'+str(port)
  170. # Get the filename of the capability
  171. ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,updateActor,'accept')
  172. if not ocapFilename:
  173. return None
  174. # The capability should already exist for it to be updated
  175. if not os.path.isfile(ocapFilename):
  176. return None
  177. # create an update activity
  178. ocapUpdate = {
  179. "@context": "https://www.w3.org/ns/activitystreams",
  180. 'type': 'Update',
  181. 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
  182. 'to': [updateActor],
  183. 'cc': [],
  184. 'object': {}
  185. }
  186. # read the existing capability
  187. with open(ocapFilename, 'r') as fp:
  188. ocapJson=commentjson.load(fp)
  189. # set the new capabilities list. eg. ["inbox:write","objects:read"]
  190. ocapJson['capability']=updateCaps
  191. # change the id, so that the old capabilities can't continue to be used
  192. updateActorNickname=getNicknameFromActor(updateActor)
  193. if not updateActorNickname:
  194. print('WARN: unable to find nickname in '+updateActor)
  195. return None
  196. updateActorDomain,updateActorPort=getDomainFromActor(updateActor)
  197. if updateActorPort:
  198. ocapId=updateActorNickname+'@'+updateActorDomain+':'+str(updateActorPort)+'#'+createPassword(32)
  199. else:
  200. ocapId=updateActorNickname+'@'+updateActorDomain+'#'+createPassword(32)
  201. ocapJson['id']=httpPrefix+"://"+fullDomain+"/caps/"+ocapId
  202. ocapUpdate['object']=ocapJson
  203. # save it again
  204. with open(ocapFilename, 'w') as fp:
  205. commentjson.dump(ocapJson, fp, indent=4, sort_keys=False)
  206. return ocapUpdate
  207. def capabilitiesReceiveUpdate(baseDir :str, \
  208. nickname :str,domain :str,port :int, \
  209. actor :str, \
  210. newCapabilitiesId :str, \
  211. capabilityList :[], debug :bool) -> bool:
  212. """An update for a capability or the given actor has arrived
  213. """
  214. ocapFilename= \
  215. getOcapFilename(baseDir,nickname,domain,actor,'granted')
  216. if not ocapFilename:
  217. return False
  218. if not os.path.isfile(ocapFilename):
  219. if debug:
  220. print('DEBUG: capabilities file not found during update')
  221. print(ocapFilename)
  222. return False
  223. with open(ocapFilename, 'r') as fp:
  224. ocapJson=commentjson.load(fp)
  225. ocapJson['id']=newCapabilitiesId
  226. ocapJson['capability']=capabilityList
  227. with open(ocapFilename, 'w') as fp:
  228. commentjson.dump(ocapJson, fp, indent=4, sort_keys=False)
  229. return True
  230. return False