capabilities.py 9.3 KB

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