shares.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. __filename__ = "shares.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 json
  9. import commentjson
  10. import os
  11. import time
  12. from shutil import copyfile
  13. from webfinger import webfingerHandle
  14. from auth import createBasicAuthHeader
  15. from posts import getPersonBox
  16. from session import postJson
  17. from utils import validNickname
  18. from utils import getNicknameFromActor
  19. from utils import getDomainFromActor
  20. from media import removeMetaData
  21. def removeShare(baseDir: str,nickname: str,domain: str, \
  22. displayName: str) -> None:
  23. """Removes a share for a person
  24. """
  25. sharesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json'
  26. if os.path.isfile(sharesFilename):
  27. with open(sharesFilename, 'r') as fp:
  28. sharesJson=commentjson.load(fp)
  29. itemID=displayName.replace(' ','')
  30. if sharesJson.get(itemID):
  31. # remove any image for the item
  32. itemIDfile=baseDir+'/sharefiles/'+itemID
  33. if sharesJson[itemID]['imageUrl']:
  34. if sharesJson[itemID]['imageUrl'].endswith('.png'):
  35. os.remove(itemIDfile+'.png')
  36. if sharesJson[itemID]['imageUrl'].endswith('.jpg'):
  37. os.remove(itemIDfile+'.jpg')
  38. if sharesJson[itemID]['imageUrl'].endswith('.gif'):
  39. os.remove(itemIDfile+'.gif')
  40. # remove the item itself
  41. del sharesJson[itemID]
  42. with open(sharesFilename, 'w') as fp:
  43. commentjson.dump(sharesJson, fp, indent=4, sort_keys=False)
  44. def addShare(baseDir: str, \
  45. httpPrefix: str,nickname: str,domain: str,port: int, \
  46. displayName: str, \
  47. summary: str, \
  48. imageFilename: str, \
  49. itemType: str, \
  50. itemCategory: str, \
  51. location: str, \
  52. duration: str,
  53. debug: bool) -> None:
  54. """Updates the likes collection within a post
  55. """
  56. sharesFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/shares.json'
  57. sharesJson={}
  58. if os.path.isfile(sharesFilename):
  59. with open(sharesFilename, 'r') as fp:
  60. sharesJson=commentjson.load(fp)
  61. duration=duration.lower()
  62. durationSec=0
  63. published=int(time.time())
  64. if ' ' in duration:
  65. durationList=duration.split(' ')
  66. if durationList[0].isdigit():
  67. if 'hour' in durationList[1]:
  68. durationSec=published+(int(durationList[0])*60*60)
  69. if 'day' in durationList[1]:
  70. durationSec=published+(int(durationList[0])*60*60*24)
  71. if 'week' in durationList[1]:
  72. durationSec=published+(int(durationList[0])*60*60*24*7)
  73. if 'month' in durationList[1]:
  74. durationSec=published+(int(durationList[0])*60*60*24*30)
  75. if 'year' in durationList[1]:
  76. durationSec=published+(int(durationList[0])*60*60*24*365)
  77. itemID=displayName.replace(' ','')
  78. # has an image for this share been uploaded?
  79. imageUrl=None
  80. moveImage=False
  81. if not imageFilename:
  82. sharesImageFilename=baseDir+'/accounts/'+nickname+'@'+domain+'/upload'
  83. if os.path.isfile(sharesImageFilename+'.png'):
  84. imageFilename=sharesImageFilename+'.png'
  85. moveImage=True
  86. elif os.path.isfile(sharesImageFilename+'.jpg'):
  87. imageFilename=sharesImageFilename+'.jpg'
  88. moveImage=True
  89. elif os.path.isfile(sharesImageFilename+'.gif'):
  90. imageFilename=sharesImageFilename+'.gif'
  91. moveImage=True
  92. # copy or move the image for the shared item to its destination
  93. if imageFilename:
  94. if os.path.isfile(imageFilename):
  95. domainFull=domain
  96. if port:
  97. if port!=80 and port!=443:
  98. if ':' not in domain:
  99. domainFull=domain+':'+str(port)
  100. if not os.path.isdir(baseDir+'/sharefiles'):
  101. os.mkdir(baseDir+'/sharefiles')
  102. if not os.path.isdir(baseDir+'/sharefiles/'+nickname):
  103. os.mkdir(baseDir+'/sharefiles/'+nickname)
  104. itemIDfile=baseDir+'/sharefiles/'+nickname+'/'+itemID
  105. if imageFilename.endswith('.png'):
  106. removeMetaData(imageFilename,itemIDfile+'.png')
  107. if moveImage:
  108. os.remove(imageFilename)
  109. imageUrl=httpPrefix+'://'+domainFull+'/sharefiles/'+nickname+'/'+itemID+'.png'
  110. if imageFilename.endswith('.jpg'):
  111. removeMetaData(imageFilename,itemIDfile+'.jpg')
  112. if moveImage:
  113. os.remove(imageFilename)
  114. imageUrl=httpPrefix+'://'+domainFull+'/sharefiles/'+nickname+'/'+itemID+'.jpg'
  115. if imageFilename.endswith('.gif'):
  116. removeMetaData(imageFilename,itemIDfile+'.gif')
  117. if moveImage:
  118. os.remove(imageFilename)
  119. imageUrl=httpPrefix+'://'+domainFull+'/sharefiles/'+nickname+'/'+itemID+'.gif'
  120. sharesJson[itemID] = {
  121. "displayName": displayName,
  122. "summary": summary,
  123. "imageUrl": imageUrl,
  124. "itemType": itemType,
  125. "category": itemCategory,
  126. "location": location,
  127. "published": published,
  128. "expire": durationSec
  129. }
  130. with open(sharesFilename, 'w') as fp:
  131. commentjson.dump(sharesJson, fp, indent=4, sort_keys=False)
  132. def expireShares(baseDir: str,nickname: str,domain: str) -> None:
  133. """Removes expired items from shares
  134. """
  135. handleDomain=domain
  136. if ':' in handleDomain:
  137. handleDomain=domain.split(':')[0]
  138. handle=nickname+'@'+handleDomain
  139. sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
  140. if os.path.isfile(sharesFilename):
  141. with open(sharesFilename, 'r') as fp:
  142. sharesJson=commentjson.load(fp)
  143. currTime=int(time.time())
  144. deleteItemID=[]
  145. for itemID,item in sharesJson.items():
  146. if currTime>item['expire']:
  147. deleteItemID.append(itemID)
  148. if deleteItemID:
  149. for itemID in deleteItemID:
  150. del sharesJson[itemID]
  151. # remove any associated images
  152. itemIDfile=baseDir+'/sharefiles/'+nickname+'/'+itemID
  153. if os.path.isfile(itemIDfile+'.png'):
  154. os.remove(itemIDfile+'.png')
  155. if os.path.isfile(itemIDfile+'.jpg'):
  156. os.remove(itemIDfile+'.jpg')
  157. if os.path.isfile(itemIDfile+'.gif'):
  158. os.remove(itemIDfile+'.gif')
  159. with open(sharesFilename, 'w') as fp:
  160. commentjson.dump(sharesJson, fp, indent=4, sort_keys=False)
  161. def getSharesFeedForPerson(baseDir: str, \
  162. domain: str,port: int, \
  163. path: str,httpPrefix: str, \
  164. sharesPerPage=12) -> {}:
  165. """Returns the shares for an account from GET requests
  166. """
  167. if '/shares' not in path:
  168. return None
  169. # handle page numbers
  170. headerOnly=True
  171. pageNumber=None
  172. if '?page=' in path:
  173. pageNumber=path.split('?page=')[1]
  174. if pageNumber=='true':
  175. pageNumber=1
  176. else:
  177. try:
  178. pageNumber=int(pageNumber)
  179. except:
  180. pass
  181. path=path.split('?page=')[0]
  182. headerOnly=False
  183. if not path.endswith('/shares'):
  184. return None
  185. nickname=None
  186. if path.startswith('/users/'):
  187. nickname=path.replace('/users/','',1).replace('/shares','')
  188. if path.startswith('/@'):
  189. nickname=path.replace('/@','',1).replace('/shares','')
  190. if not nickname:
  191. return None
  192. if not validNickname(domain,nickname):
  193. return None
  194. if port:
  195. if port!=80 and port!=443:
  196. if ':' not in domain:
  197. domain=domain+':'+str(port)
  198. handleDomain=domain
  199. if ':' in handleDomain:
  200. handleDomain=domain.split(':')[0]
  201. handle=nickname+'@'+handleDomain
  202. sharesFilename=baseDir+'/accounts/'+handle+'/shares.json'
  203. if headerOnly:
  204. noOfShares=0
  205. if os.path.isfile(sharesFilename):
  206. with open(sharesFilename, 'r') as fp:
  207. sharesJson=commentjson.load(fp)
  208. noOfShares=len(sharesJson.items())
  209. shares = {
  210. '@context': 'https://www.w3.org/ns/activitystreams',
  211. 'first': httpPrefix+'://'+domain+'/users/'+nickname+'/shares?page=1',
  212. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/shares',
  213. 'totalItems': str(noOfShares),
  214. 'type': 'OrderedCollection'}
  215. return shares
  216. if not pageNumber:
  217. pageNumber=1
  218. nextPageNumber=int(pageNumber+1)
  219. shares = {
  220. '@context': 'https://www.w3.org/ns/activitystreams',
  221. 'id': httpPrefix+'://'+domain+'/users/'+nickname+'/shares?page='+str(pageNumber),
  222. 'orderedItems': [],
  223. 'partOf': httpPrefix+'://'+domain+'/users/'+nickname+'/shares',
  224. 'totalItems': 0,
  225. 'type': 'OrderedCollectionPage'}
  226. if not os.path.isfile(sharesFilename):
  227. print("test5")
  228. return shares
  229. currPage=1
  230. pageCtr=0
  231. totalCtr=0
  232. with open(sharesFilename, 'r') as fp:
  233. sharesJson=commentjson.load(fp)
  234. for itemID,item in sharesJson.items():
  235. pageCtr += 1
  236. totalCtr += 1
  237. if currPage==pageNumber:
  238. shares['orderedItems'].append(item)
  239. if pageCtr>=sharesPerPage:
  240. pageCtr=0
  241. currPage += 1
  242. shares['totalItems']=totalCtr
  243. lastPage=int(totalCtr/sharesPerPage)
  244. if lastPage<1:
  245. lastPage=1
  246. if nextPageNumber>lastPage:
  247. shares['next']=httpPrefix+'://'+domain+'/users/'+nickname+'/shares?page='+str(lastPage)
  248. return shares
  249. def sendShareViaServer(baseDir,session, \
  250. fromNickname: str,password: str, \
  251. fromDomain: str,fromPort: int, \
  252. httpPrefix: str, \
  253. displayName: str, \
  254. summary: str, \
  255. imageFilename: str, \
  256. itemType: str, \
  257. itemCategory: str, \
  258. location: str, \
  259. duration: str, \
  260. cachedWebfingers: {},personCache: {}, \
  261. debug: bool, \
  262. projectVersion: str) -> {}:
  263. """Creates an item share via c2s
  264. """
  265. if not session:
  266. print('WARN: No session for sendShareViaServer')
  267. return 6
  268. fromDomainFull=fromDomain
  269. if fromPort:
  270. if fromPort!=80 and fromPort!=443:
  271. if ':' not in fromDomain:
  272. fromDomainFull=fromDomain+':'+str(fromPort)
  273. toUrl = 'https://www.w3.org/ns/activitystreams#Public'
  274. ccUrl = httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers'
  275. newShareJson = {
  276. "@context": "https://www.w3.org/ns/activitystreams",
  277. 'type': 'Add',
  278. 'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname,
  279. 'target': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/shares',
  280. 'object': {
  281. "type": "Offer",
  282. "displayName": displayName,
  283. "summary": summary,
  284. "itemType": itemType,
  285. "category": category,
  286. "location": location,
  287. "duration": duration,
  288. 'to': [toUrl],
  289. 'cc': [ccUrl]
  290. },
  291. 'to': [toUrl],
  292. 'cc': [ccUrl]
  293. }
  294. handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
  295. # lookup the inbox for the To handle
  296. wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  297. fromDomain,projectVersion)
  298. if not wfRequest:
  299. if debug:
  300. print('DEBUG: announce webfinger failed for '+handle)
  301. return 1
  302. postToBox='outbox'
  303. # get the actor inbox for the To handle
  304. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
  305. getPersonBox(baseDir,session,wfRequest,personCache, \
  306. projectVersion,httpPrefix,fromDomain,postToBox)
  307. if not inboxUrl:
  308. if debug:
  309. print('DEBUG: No '+postToBox+' was found for '+handle)
  310. return 3
  311. if not fromPersonId:
  312. if debug:
  313. print('DEBUG: No actor was found for '+handle)
  314. return 4
  315. authHeader=createBasicAuthHeader(fromNickname,password)
  316. if imageFilename:
  317. headers = {'host': fromDomain, \
  318. 'Authorization': authHeader}
  319. postResult = \
  320. postImage(session,imageFilename,[],inboxUrl.replace('/'+postToBox,'/shares'),headers,"inbox:write")
  321. headers = {'host': fromDomain, \
  322. 'Content-type': 'application/json', \
  323. 'Authorization': authHeader}
  324. postResult = \
  325. postJson(session,newShareJson,[],inboxUrl,headers,"inbox:write")
  326. #if not postResult:
  327. # if debug:
  328. # print('DEBUG: POST announce failed for c2s to '+inboxUrl)
  329. # return 5
  330. if debug:
  331. print('DEBUG: c2s POST share item success')
  332. return newShareJson
  333. def sendUndoShareViaServer(baseDir: str,session, \
  334. fromNickname: str,password: str, \
  335. fromDomain: str,fromPort: int, \
  336. httpPrefix: str, \
  337. displayName: str, \
  338. cachedWebfingers: {},personCache: {}, \
  339. debug: bool,projectVersion: str) -> {}:
  340. """Undoes a share via c2s
  341. """
  342. if not session:
  343. print('WARN: No session for sendUndoShareViaServer')
  344. return 6
  345. fromDomainFull=fromDomain
  346. if fromPort:
  347. if fromPort!=80 and fromPort!=443:
  348. if ':' not in fromDomain:
  349. fromDomainFull=fromDomain+':'+str(fromPort)
  350. toUrl = 'https://www.w3.org/ns/activitystreams#Public'
  351. ccUrl = httpPrefix + '://'+fromDomainFull+'/users/'+fromNickname+'/followers'
  352. undoShareJson = {
  353. "@context": "https://www.w3.org/ns/activitystreams",
  354. 'type': 'Remove',
  355. 'actor': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname,
  356. 'target': httpPrefix+'://'+fromDomainFull+'/users/'+fromNickname+'/shares',
  357. 'object': {
  358. "type": "Offer",
  359. "displayName": displayName,
  360. 'to': [toUrl],
  361. 'cc': [ccUrl]
  362. },
  363. 'to': [toUrl],
  364. 'cc': [ccUrl]
  365. }
  366. handle=httpPrefix+'://'+fromDomainFull+'/@'+fromNickname
  367. # lookup the inbox for the To handle
  368. wfRequest = webfingerHandle(session,handle,httpPrefix,cachedWebfingers, \
  369. fromDomain,projectVersion)
  370. if not wfRequest:
  371. if debug:
  372. print('DEBUG: announce webfinger failed for '+handle)
  373. return 1
  374. postToBox='outbox'
  375. # get the actor inbox for the To handle
  376. inboxUrl,pubKeyId,pubKey,fromPersonId,sharedInbox,capabilityAcquisition,avatarUrl,displayName = \
  377. getPersonBox(baseDir,session,wfRequest,personCache, \
  378. projectVersion,httpPrefix,fromDomain,postToBox)
  379. if not inboxUrl:
  380. if debug:
  381. print('DEBUG: No '+postToBox+' was found for '+handle)
  382. return 3
  383. if not fromPersonId:
  384. if debug:
  385. print('DEBUG: No actor was found for '+handle)
  386. return 4
  387. authHeader=createBasicAuthHeader(fromNickname,password)
  388. headers = {'host': fromDomain, \
  389. 'Content-type': 'application/json', \
  390. 'Authorization': authHeader}
  391. postResult = \
  392. postJson(session,undoShareJson,[],inboxUrl,headers,"inbox:write")
  393. #if not postResult:
  394. # if debug:
  395. # print('DEBUG: POST announce failed for c2s to '+inboxUrl)
  396. # return 5
  397. if debug:
  398. print('DEBUG: c2s POST undo share success')
  399. return undoShareJson
  400. def outboxShareUpload(baseDir: str,httpPrefix: str, \
  401. nickname: str,domain: str,port: int, \
  402. messageJson: {},debug: bool) -> None:
  403. """ When a shared item is received by the outbox from c2s
  404. """
  405. if not messageJson.get('type'):
  406. return
  407. if not messageJson['type']=='Add':
  408. return
  409. if not messageJson.get('object'):
  410. return
  411. if not isinstance(messageJson['object'], dict):
  412. return
  413. if not messageJson['object'].get('type'):
  414. if debug:
  415. print('DEBUG: undo block - no type')
  416. return
  417. if not messageJson['object']['type']=='Offer':
  418. if debug:
  419. print('DEBUG: not an Offer activity')
  420. return
  421. if not messageJson['object'].get('displayName'):
  422. if debug:
  423. print('DEBUG: displayName missing from Offer')
  424. return
  425. if not messageJson['object'].get('summary'):
  426. if debug:
  427. print('DEBUG: summary missing from Offer')
  428. return
  429. if not messageJson['object'].get('itemType'):
  430. if debug:
  431. print('DEBUG: itemType missing from Offer')
  432. return
  433. if not messageJson['object'].get('category'):
  434. if debug:
  435. print('DEBUG: category missing from Offer')
  436. return
  437. if not messageJson['object'].get('location'):
  438. if debug:
  439. print('DEBUG: location missing from Offer')
  440. return
  441. if not messageJson['object'].get('duration'):
  442. if debug:
  443. print('DEBUG: duration missing from Offer')
  444. return
  445. addShare(baseDir, \
  446. httpPrefix,nickname,domain,port, \
  447. messageJson['object']['displayName'], \
  448. messageJson['object']['summary'], \
  449. messageJson['object']['imageFilename'], \
  450. messageJson['object']['itemType'], \
  451. messageJson['object']['itemCategory'], \
  452. messageJson['object']['location'], \
  453. messageJson['object']['duration'], \
  454. debug)
  455. if debug:
  456. print('DEBUG: shared item received via c2s')
  457. def outboxUndoShareUpload(baseDir: str,httpPrefix: str, \
  458. nickname: str,domain: str,port: int, \
  459. messageJson: {},debug: bool) -> None:
  460. """ When a shared item is removed via c2s
  461. """
  462. if not messageJson.get('type'):
  463. return
  464. if not messageJson['type']=='Remove':
  465. return
  466. if not messageJson.get('object'):
  467. return
  468. if not isinstance(messageJson['object'], dict):
  469. return
  470. if not messageJson['object'].get('type'):
  471. if debug:
  472. print('DEBUG: undo block - no type')
  473. return
  474. if not messageJson['object']['type']=='Offer':
  475. if debug:
  476. print('DEBUG: not an Offer activity')
  477. return
  478. if not messageJson['object'].get('displayName'):
  479. if debug:
  480. print('DEBUG: displayName missing from Offer')
  481. return
  482. removeShare(baseDir,nickname,domain, \
  483. messageJson['object']['displayName'])
  484. if debug:
  485. print('DEBUG: shared item removed via c2s')