capabilities.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. __filename__ = "capabilities.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "0.0.1"
  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. acceptedActorDomain,acceptedActorPort=getDomainFromActor(acceptedActor)
  121. if acceptedActorPort:
  122. ocapId=acceptedActorNickname+'@'+acceptedActorDomain+':'+str(acceptedActorPort)+'#'+createPassword(32)
  123. else:
  124. ocapId=acceptedActorNickname+'@'+acceptedActorDomain+'#'+createPassword(32)
  125. ocapAccept = {
  126. "@context": "https://www.w3.org/ns/activitystreams",
  127. "id": httpPrefix+"://"+fullDomain+"/caps/"+ocapId,
  128. "type": "Capability",
  129. "capability": acceptedCaps,
  130. "scope": acceptedActor,
  131. "actor": httpPrefix+"://"+fullDomain
  132. }
  133. if nickname:
  134. ocapAccept['actor']=httpPrefix+"://"+fullDomain+'/users/'+nickname
  135. if saveToFile:
  136. with open(ocapFilename, 'w') as fp:
  137. commentjson.dump(ocapAccept, fp, indent=4, sort_keys=False)
  138. return ocapAccept
  139. def capabilitiesGrantedSave(baseDir :str,nickname :str,domain :str,ocap: {}) -> bool:
  140. """A capabilities accept is received, so stor it for
  141. reference when sending to the actor
  142. """
  143. if not ocap.get('actor'):
  144. return False
  145. ocapFilename=getOcapFilename(baseDir,nickname,domain,ocap['actor'],'granted')
  146. if not ocapFilename:
  147. return False
  148. with open(ocapFilename, 'w') as fp:
  149. commentjson.dump(ocap, fp, indent=4, sort_keys=False)
  150. return True
  151. def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
  152. nickname: str,domain: str, port: int, \
  153. updateActor: str, \
  154. updateCaps: []) -> {}:
  155. """Used to sends an update for a change of object capabilities
  156. Note that the capability id gets changed with a new random token
  157. so that the old capabilities can't continue to be used
  158. """
  159. # reject excessively long actors
  160. if len(updateActor)>256:
  161. return None
  162. fullDomain=domain
  163. if port:
  164. if port!=80 and port !=443:
  165. if ':' not in domain:
  166. fullDomain=domain+':'+str(port)
  167. # Get the filename of the capability
  168. ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,updateActor,'accept')
  169. if not ocapFilename:
  170. return None
  171. # The capability should already exist for it to be updated
  172. if not os.path.isfile(ocapFilename):
  173. return None
  174. # create an update activity
  175. ocapUpdate = {
  176. "@context": "https://www.w3.org/ns/activitystreams",
  177. 'type': 'Update',
  178. 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
  179. 'to': [updateActor],
  180. 'cc': [],
  181. 'object': {}
  182. }
  183. # read the existing capability
  184. with open(ocapFilename, 'r') as fp:
  185. ocapJson=commentjson.load(fp)
  186. # set the new capabilities list. eg. ["inbox:write","objects:read"]
  187. ocapJson['capability']=updateCaps
  188. # change the id, so that the old capabilities can't continue to be used
  189. updateActorNickname=getNicknameFromActor(updateActor)
  190. updateActorDomain,updateActorPort=getDomainFromActor(updateActor)
  191. if updateActorPort:
  192. ocapId=updateActorNickname+'@'+updateActorDomain+':'+str(updateActorPort)+'#'+createPassword(32)
  193. else:
  194. ocapId=updateActorNickname+'@'+updateActorDomain+'#'+createPassword(32)
  195. ocapJson['id']=httpPrefix+"://"+fullDomain+"/caps/"+ocapId
  196. ocapUpdate['object']=ocapJson
  197. # save it again
  198. with open(ocapFilename, 'w') as fp:
  199. commentjson.dump(ocapJson, fp, indent=4, sort_keys=False)
  200. return ocapUpdate
  201. def capabilitiesReceiveUpdate(baseDir :str, \
  202. nickname :str,domain :str,port :int, \
  203. actor :str, \
  204. newCapabilitiesId :str, \
  205. capabilityList :[], debug :bool) -> bool:
  206. """An update for a capability or the given actor has arrived
  207. """
  208. ocapFilename= \
  209. getOcapFilename(baseDir,nickname,domain,actor,'granted')
  210. if not ocapFilename:
  211. return False
  212. if not os.path.isfile(ocapFilename):
  213. if debug:
  214. print('DEBUG: capabilities file not found during update')
  215. print(ocapFilename)
  216. return False
  217. with open(ocapFilename, 'r') as fp:
  218. ocapJson=commentjson.load(fp)
  219. ocapJson['id']=newCapabilitiesId
  220. ocapJson['capability']=capabilityList
  221. with open(ocapFilename, 'w') as fp:
  222. commentjson.dump(ocapJson, fp, indent=4, sort_keys=False)
  223. return True
  224. return False