capabilities.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  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. from utils import loadJson
  17. from utils import saveJson
  18. def getOcapFilename(baseDir :str,nickname: str,domain: str,actor :str,subdir: str) -> str:
  19. """Returns the filename for a particular capability accepted or granted
  20. Also creates directories as needed
  21. """
  22. if not actor:
  23. return None
  24. if ':' in domain:
  25. domain=domain.split(':')[0]
  26. if not os.path.isdir(baseDir+'/accounts'):
  27. os.mkdir(baseDir+'/accounts')
  28. ocDir=baseDir+'/accounts/'+nickname+'@'+domain
  29. if not os.path.isdir(ocDir):
  30. os.mkdir(ocDir)
  31. ocDir=baseDir+'/accounts/'+nickname+'@'+domain+'/ocap'
  32. if not os.path.isdir(ocDir):
  33. os.mkdir(ocDir)
  34. ocDir=baseDir+'/accounts/'+nickname+'@'+domain+'/ocap/'+subdir
  35. if not os.path.isdir(ocDir):
  36. os.mkdir(ocDir)
  37. return baseDir+'/accounts/'+nickname+'@'+domain+'/ocap/'+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=getOcapFilename(baseDir,nickname,fullDomain,acceptedActor,'accept')
  112. if not ocapFilename:
  113. return None
  114. ocapAccept=None
  115. # if the capability already exists then load it from file
  116. if os.path.isfile(ocapFilename):
  117. ocapAccept=loadJson(ocapFilename)
  118. # otherwise create a new capability
  119. if not ocapAccept:
  120. acceptedActorNickname=getNicknameFromActor(acceptedActor)
  121. if not acceptedActorNickname:
  122. print('WARN: unable to find nickname in '+acceptedActor)
  123. return None
  124. acceptedActorDomain,acceptedActorPort=getDomainFromActor(acceptedActor)
  125. if acceptedActorPort:
  126. ocapId=acceptedActorNickname+'@'+acceptedActorDomain+':'+str(acceptedActorPort)+'#'+createPassword(32)
  127. else:
  128. ocapId=acceptedActorNickname+'@'+acceptedActorDomain+'#'+createPassword(32)
  129. ocapAccept = {
  130. "@context": "https://www.w3.org/ns/activitystreams",
  131. "id": httpPrefix+"://"+fullDomain+"/caps/"+ocapId,
  132. "type": "Capability",
  133. "capability": acceptedCaps,
  134. "scope": acceptedActor,
  135. "actor": httpPrefix+"://"+fullDomain
  136. }
  137. if nickname:
  138. ocapAccept['actor']=httpPrefix+"://"+fullDomain+'/users/'+nickname
  139. if saveToFile:
  140. saveJson(ocapAccept,ocapFilename)
  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. saveJson(ocap,ocapFilename)
  152. return True
  153. def capabilitiesUpdate(baseDir: str,httpPrefix: str, \
  154. nickname: str,domain: str, port: int, \
  155. updateActor: str, \
  156. updateCaps: []) -> {}:
  157. """Used to sends an update for a change of object capabilities
  158. Note that the capability id gets changed with a new random token
  159. so that the old capabilities can't continue to be used
  160. """
  161. # reject excessively long actors
  162. if len(updateActor)>256:
  163. return None
  164. fullDomain=domain
  165. if port:
  166. if port!=80 and port !=443:
  167. if ':' not in domain:
  168. fullDomain=domain+':'+str(port)
  169. # Get the filename of the capability
  170. ocapFilename=getOcapFilename(baseDir,nickname,fullDomain,updateActor,'accept')
  171. if not ocapFilename:
  172. return None
  173. # The capability should already exist for it to be updated
  174. if not os.path.isfile(ocapFilename):
  175. return None
  176. # create an update activity
  177. ocapUpdate = {
  178. "@context": "https://www.w3.org/ns/activitystreams",
  179. 'type': 'Update',
  180. 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
  181. 'to': [updateActor],
  182. 'cc': [],
  183. 'object': {}
  184. }
  185. # read the existing capability
  186. ocapJson=loadJson(ocapFilename)
  187. # set the new capabilities list. eg. ["inbox:write","objects:read"]
  188. ocapJson['capability']=updateCaps
  189. # change the id, so that the old capabilities can't continue to be used
  190. updateActorNickname=getNicknameFromActor(updateActor)
  191. if not updateActorNickname:
  192. print('WARN: unable to find nickname in '+updateActor)
  193. return None
  194. updateActorDomain,updateActorPort=getDomainFromActor(updateActor)
  195. if updateActorPort:
  196. ocapId=updateActorNickname+'@'+updateActorDomain+':'+str(updateActorPort)+'#'+createPassword(32)
  197. else:
  198. ocapId=updateActorNickname+'@'+updateActorDomain+'#'+createPassword(32)
  199. ocapJson['id']=httpPrefix+"://"+fullDomain+"/caps/"+ocapId
  200. ocapUpdate['object']=ocapJson
  201. # save it again
  202. saveJson(ocapJson,ocapFilename)
  203. return ocapUpdate
  204. def capabilitiesReceiveUpdate(baseDir :str, \
  205. nickname :str,domain :str,port :int, \
  206. actor :str, \
  207. newCapabilitiesId :str, \
  208. capabilityList :[], debug :bool) -> bool:
  209. """An update for a capability or the given actor has arrived
  210. """
  211. ocapFilename= \
  212. getOcapFilename(baseDir,nickname,domain,actor,'granted')
  213. if not ocapFilename:
  214. return False
  215. if not os.path.isfile(ocapFilename):
  216. if debug:
  217. print('DEBUG: capabilities file not found during update')
  218. print(ocapFilename)
  219. return False
  220. ocapJson=loadJson(ocapFilename)
  221. if ocapJson:
  222. ocapJson['id']=newCapabilitiesId
  223. ocapJson['capability']=capabilityList
  224. return saveJson(ocapJson,ocapFilename)
  225. return False