capabilities.py 9.8 KB

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