like.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. __filename__ = "like.py"
  2. __author__ = "Bob Mottram"
  3. __license__ = "AGPL3+"
  4. __version__ = "1.1.0"
  5. __maintainer__ = "Bob Mottram"
  6. __email__ = "bob@freedombone.net"
  7. __status__ = "Production"
  8. from utils import urlPermitted
  9. from utils import getNicknameFromActor
  10. from utils import getDomainFromActor
  11. from utils import locatePost
  12. from utils import updateLikesCollection
  13. from utils import undoLikesCollectionEntry
  14. from posts import sendSignedJson
  15. from session import postJson
  16. from webfinger import webfingerHandle
  17. from auth import createBasicAuthHeader
  18. from posts import getPersonBox
  19. def likedByPerson(postJsonObject: {}, nickname: str, domain: str) -> bool:
  20. """Returns True if the given post is liked by the given person
  21. """
  22. if noOfLikes(postJsonObject) == 0:
  23. return False
  24. actorMatch = domain+'/users/'+nickname
  25. for item in postJsonObject['object']['likes']['items']:
  26. if item['actor'].endswith(actorMatch):
  27. return True
  28. return False
  29. def noOfLikes(postJsonObject: {}) -> int:
  30. """Returns the number of likes ona given post
  31. """
  32. if not postJsonObject.get('object'):
  33. return 0
  34. if not isinstance(postJsonObject['object'], dict):
  35. return 0
  36. if not postJsonObject['object'].get('likes'):
  37. return 0
  38. if not isinstance(postJsonObject['object']['likes'], dict):
  39. return 0
  40. if not postJsonObject['object']['likes'].get('items'):
  41. postJsonObject['object']['likes']['items'] = []
  42. postJsonObject['object']['likes']['totalItems'] = 0
  43. return len(postJsonObject['object']['likes']['items'])
  44. def like(recentPostsCache: {},
  45. session, baseDir: str, federationList: [],
  46. nickname: str, domain: str, port: int,
  47. ccList: [], httpPrefix: str,
  48. objectUrl: str, actorLiked: str,
  49. clientToServer: bool,
  50. sendThreads: [], postLog: [],
  51. personCache: {}, cachedWebfingers: {},
  52. debug: bool, projectVersion: str) -> {}:
  53. """Creates a like
  54. actor is the person doing the liking
  55. 'to' might be a specific person (actor) whose post was liked
  56. object is typically the url of the message which was liked
  57. """
  58. if not urlPermitted(objectUrl, federationList, "inbox:write"):
  59. return None
  60. fullDomain = domain
  61. if port:
  62. if port != 80 and port != 443:
  63. if ':' not in domain:
  64. fullDomain = domain+':'+str(port)
  65. newLikeJson = {
  66. "@context": "https://www.w3.org/ns/activitystreams",
  67. 'type': 'Like',
  68. 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
  69. 'object': objectUrl
  70. }
  71. if ccList:
  72. if len(ccList) > 0:
  73. newLikeJson['cc'] = ccList
  74. # Extract the domain and nickname from a statuses link
  75. likedPostNickname = None
  76. likedPostDomain = None
  77. likedPostPort = None
  78. if actorLiked:
  79. likedPostNickname = getNicknameFromActor(actorLiked)
  80. likedPostDomain, likedPostPort = getDomainFromActor(actorLiked)
  81. else:
  82. if '/users/' in objectUrl or \
  83. '/accounts/' in objectUrl or \
  84. '/channel/' in objectUrl or \
  85. '/profile/' in objectUrl:
  86. likedPostNickname = getNicknameFromActor(objectUrl)
  87. likedPostDomain, likedPostPort = getDomainFromActor(objectUrl)
  88. if likedPostNickname:
  89. postFilename = locatePost(baseDir, nickname, domain, objectUrl)
  90. if not postFilename:
  91. print('DEBUG: like baseDir: ' + baseDir)
  92. print('DEBUG: like nickname: ' + nickname)
  93. print('DEBUG: like domain: ' + domain)
  94. print('DEBUG: like objectUrl: ' + objectUrl)
  95. return None
  96. updateLikesCollection(recentPostsCache,
  97. baseDir, postFilename, objectUrl,
  98. newLikeJson['actor'], domain, debug)
  99. sendSignedJson(newLikeJson, session, baseDir,
  100. nickname, domain, port,
  101. likedPostNickname, likedPostDomain, likedPostPort,
  102. 'https://www.w3.org/ns/activitystreams#Public',
  103. httpPrefix, True, clientToServer, federationList,
  104. sendThreads, postLog, cachedWebfingers, personCache,
  105. debug, projectVersion)
  106. return newLikeJson
  107. def likePost(recentPostsCache: {},
  108. session, baseDir: str, federationList: [],
  109. nickname: str, domain: str, port: int, httpPrefix: str,
  110. likeNickname: str, likeDomain: str, likePort: int,
  111. ccList: [],
  112. likeStatusNumber: int, clientToServer: bool,
  113. sendThreads: [], postLog: [],
  114. personCache: {}, cachedWebfingers: {},
  115. debug: bool, projectVersion: str) -> {}:
  116. """Likes a given status post. This is only used by unit tests
  117. """
  118. likeDomain = likeDomain
  119. if likePort:
  120. if likePort != 80 and likePort != 443:
  121. if ':' not in likeDomain:
  122. likeDomain = likeDomain + ':' + str(likePort)
  123. actorLiked = httpPrefix + '://' + likeDomain + '/users/' + likeNickname
  124. objectUrl = actorLiked + '/statuses/' + str(likeStatusNumber)
  125. return like(recentPostsCache,
  126. session, baseDir, federationList, nickname, domain, port,
  127. ccList, httpPrefix, objectUrl, actorLiked, clientToServer,
  128. sendThreads, postLog, personCache, cachedWebfingers,
  129. debug, projectVersion)
  130. def undolike(recentPostsCache: {},
  131. session, baseDir: str, federationList: [],
  132. nickname: str, domain: str, port: int,
  133. ccList: [], httpPrefix: str,
  134. objectUrl: str, actorLiked: str,
  135. clientToServer: bool,
  136. sendThreads: [], postLog: [],
  137. personCache: {}, cachedWebfingers: {},
  138. debug: bool, projectVersion: str) -> {}:
  139. """Removes a like
  140. actor is the person doing the liking
  141. 'to' might be a specific person (actor) whose post was liked
  142. object is typically the url of the message which was liked
  143. """
  144. if not urlPermitted(objectUrl, federationList, "inbox:write"):
  145. return None
  146. fullDomain = domain
  147. if port:
  148. if port != 80 and port != 443:
  149. if ':' not in domain:
  150. fullDomain = domain + ':' + str(port)
  151. newUndoLikeJson = {
  152. "@context": "https://www.w3.org/ns/activitystreams",
  153. 'type': 'Undo',
  154. 'actor': httpPrefix+'://'+fullDomain+'/users/'+nickname,
  155. 'object': {
  156. 'type': 'Like',
  157. 'actor': httpPrefix + '://' + fullDomain + '/users/' + nickname,
  158. 'object': objectUrl
  159. }
  160. }
  161. if ccList:
  162. if len(ccList) > 0:
  163. newUndoLikeJson['cc'] = ccList
  164. newUndoLikeJson['object']['cc'] = ccList
  165. # Extract the domain and nickname from a statuses link
  166. likedPostNickname = None
  167. likedPostDomain = None
  168. likedPostPort = None
  169. if actorLiked:
  170. likedPostNickname = getNicknameFromActor(actorLiked)
  171. likedPostDomain, likedPostPort = getDomainFromActor(actorLiked)
  172. else:
  173. if '/users/' in objectUrl or \
  174. '/accounts/' in objectUrl or \
  175. '/channel/' in objectUrl or \
  176. '/profile/' in objectUrl:
  177. likedPostNickname = getNicknameFromActor(objectUrl)
  178. likedPostDomain, likedPostPort = getDomainFromActor(objectUrl)
  179. if likedPostNickname:
  180. postFilename = locatePost(baseDir, nickname, domain, objectUrl)
  181. if not postFilename:
  182. return None
  183. undoLikesCollectionEntry(baseDir, postFilename, objectUrl,
  184. newUndoLikeJson['actor'], domain, debug)
  185. sendSignedJson(newUndoLikeJson, session, baseDir,
  186. nickname, domain, port,
  187. likedPostNickname, likedPostDomain, likedPostPort,
  188. 'https://www.w3.org/ns/activitystreams#Public',
  189. httpPrefix, True, clientToServer, federationList,
  190. sendThreads, postLog, cachedWebfingers, personCache,
  191. debug, projectVersion)
  192. else:
  193. return None
  194. return newUndoLikeJson
  195. def sendLikeViaServer(baseDir: str, session,
  196. fromNickname: str, password: str,
  197. fromDomain: str, fromPort: int,
  198. httpPrefix: str, likeUrl: str,
  199. cachedWebfingers: {}, personCache: {},
  200. debug: bool, projectVersion: str) -> {}:
  201. """Creates a like via c2s
  202. """
  203. if not session:
  204. print('WARN: No session for sendLikeViaServer')
  205. return 6
  206. fromDomainFull = fromDomain
  207. if fromPort:
  208. if fromPort != 80 and fromPort != 443:
  209. if ':' not in fromDomain:
  210. fromDomainFull = fromDomain + ':' + str(fromPort)
  211. actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname
  212. newLikeJson = {
  213. "@context": "https://www.w3.org/ns/activitystreams",
  214. 'type': 'Like',
  215. 'actor': actor,
  216. 'object': likeUrl
  217. }
  218. handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
  219. # lookup the inbox for the To handle
  220. wfRequest = webfingerHandle(session, handle, httpPrefix,
  221. cachedWebfingers,
  222. fromDomain, projectVersion)
  223. if not wfRequest:
  224. if debug:
  225. print('DEBUG: announce webfinger failed for ' + handle)
  226. return 1
  227. if not isinstance(wfRequest, dict):
  228. print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
  229. str(wfRequest))
  230. return 1
  231. postToBox = 'outbox'
  232. # get the actor inbox for the To handle
  233. (inboxUrl, pubKeyId, pubKey, fromPersonId,
  234. sharedInbox, capabilityAcquisition,
  235. avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
  236. personCache,
  237. projectVersion, httpPrefix,
  238. fromNickname, fromDomain,
  239. postToBox)
  240. if not inboxUrl:
  241. if debug:
  242. print('DEBUG: No ' + postToBox + ' was found for ' + handle)
  243. return 3
  244. if not fromPersonId:
  245. if debug:
  246. print('DEBUG: No actor was found for ' + handle)
  247. return 4
  248. authHeader = createBasicAuthHeader(fromNickname, password)
  249. headers = {
  250. 'host': fromDomain,
  251. 'Content-type': 'application/json',
  252. 'Authorization': authHeader
  253. }
  254. postResult = postJson(session, newLikeJson, [], inboxUrl,
  255. headers, "inbox:write")
  256. if not postResult:
  257. print('WARN: POST announce failed for c2s to ' + inboxUrl)
  258. return 5
  259. if debug:
  260. print('DEBUG: c2s POST like success')
  261. return newLikeJson
  262. def sendUndoLikeViaServer(baseDir: str, session,
  263. fromNickname: str, password: str,
  264. fromDomain: str, fromPort: int,
  265. httpPrefix: str, likeUrl: str,
  266. cachedWebfingers: {}, personCache: {},
  267. debug: bool, projectVersion: str) -> {}:
  268. """Undo a like via c2s
  269. """
  270. if not session:
  271. print('WARN: No session for sendUndoLikeViaServer')
  272. return 6
  273. fromDomainFull = fromDomain
  274. if fromPort:
  275. if fromPort != 80 and fromPort != 443:
  276. if ':' not in fromDomain:
  277. fromDomainFull = fromDomain + ':' + str(fromPort)
  278. actor = httpPrefix + '://' + fromDomainFull + '/users/' + fromNickname
  279. newUndoLikeJson = {
  280. "@context": "https://www.w3.org/ns/activitystreams",
  281. 'type': 'Undo',
  282. 'actor': actor,
  283. 'object': {
  284. 'type': 'Like',
  285. 'actor': actor,
  286. 'object': likeUrl
  287. }
  288. }
  289. handle = httpPrefix + '://' + fromDomainFull + '/@' + fromNickname
  290. # lookup the inbox for the To handle
  291. wfRequest = webfingerHandle(session, handle, httpPrefix,
  292. cachedWebfingers,
  293. fromDomain, projectVersion)
  294. if not wfRequest:
  295. if debug:
  296. print('DEBUG: announce webfinger failed for ' + handle)
  297. return 1
  298. if not isinstance(wfRequest, dict):
  299. print('WARN: Webfinger for ' + handle + ' did not return a dict. ' +
  300. str(wfRequest))
  301. return 1
  302. postToBox = 'outbox'
  303. # get the actor inbox for the To handle
  304. (inboxUrl, pubKeyId, pubKey, fromPersonId,
  305. sharedInbox, capabilityAcquisition,
  306. avatarUrl, displayName) = getPersonBox(baseDir, session, wfRequest,
  307. personCache, projectVersion,
  308. httpPrefix, fromNickname,
  309. fromDomain, postToBox)
  310. if not inboxUrl:
  311. if debug:
  312. print('DEBUG: No ' + postToBox + ' was found for ' + handle)
  313. return 3
  314. if not fromPersonId:
  315. if debug:
  316. print('DEBUG: No actor was found for ' + handle)
  317. return 4
  318. authHeader = createBasicAuthHeader(fromNickname, password)
  319. headers = {
  320. 'host': fromDomain,
  321. 'Content-type': 'application/json',
  322. 'Authorization': authHeader
  323. }
  324. postResult = postJson(session, newUndoLikeJson, [], inboxUrl,
  325. headers, "inbox:write")
  326. if not postResult:
  327. print('WARN: POST announce failed for c2s to ' + inboxUrl)
  328. return 5
  329. if debug:
  330. print('DEBUG: c2s POST undo like success')
  331. return newUndoLikeJson
  332. def outboxLike(recentPostsCache: {},
  333. baseDir: str, httpPrefix: str,
  334. nickname: str, domain: str, port: int,
  335. messageJson: {}, debug: bool) -> None:
  336. """ When a like request is received by the outbox from c2s
  337. """
  338. if not messageJson.get('type'):
  339. if debug:
  340. print('DEBUG: like - no type')
  341. return
  342. if not messageJson['type'] == 'Like':
  343. if debug:
  344. print('DEBUG: not a like')
  345. return
  346. if not messageJson.get('object'):
  347. if debug:
  348. print('DEBUG: no object in like')
  349. return
  350. if not isinstance(messageJson['object'], str):
  351. if debug:
  352. print('DEBUG: like object is not string')
  353. return
  354. if debug:
  355. print('DEBUG: c2s like request arrived in outbox')
  356. messageId = messageJson['object'].replace('/activity', '')
  357. if ':' in domain:
  358. domain = domain.split(':')[0]
  359. postFilename = locatePost(baseDir, nickname, domain, messageId)
  360. if not postFilename:
  361. if debug:
  362. print('DEBUG: c2s like post not found in inbox or outbox')
  363. print(messageId)
  364. return True
  365. updateLikesCollection(recentPostsCache,
  366. baseDir, postFilename, messageId,
  367. messageJson['actor'], domain, debug)
  368. if debug:
  369. print('DEBUG: post liked via c2s - ' + postFilename)
  370. def outboxUndoLike(recentPostsCache: {},
  371. baseDir: str, httpPrefix: str,
  372. nickname: str, domain: str, port: int,
  373. messageJson: {}, debug: bool) -> None:
  374. """ When an undo like request is received by the outbox from c2s
  375. """
  376. if not messageJson.get('type'):
  377. return
  378. if not messageJson['type'] == 'Undo':
  379. return
  380. if not messageJson.get('object'):
  381. return
  382. if not isinstance(messageJson['object'], dict):
  383. if debug:
  384. print('DEBUG: undo like object is not dict')
  385. return
  386. if not messageJson['object'].get('type'):
  387. if debug:
  388. print('DEBUG: undo like - no type')
  389. return
  390. if not messageJson['object']['type'] == 'Like':
  391. if debug:
  392. print('DEBUG: not a undo like')
  393. return
  394. if not messageJson['object'].get('object'):
  395. if debug:
  396. print('DEBUG: no object in undo like')
  397. return
  398. if not isinstance(messageJson['object']['object'], str):
  399. if debug:
  400. print('DEBUG: undo like object is not string')
  401. return
  402. if debug:
  403. print('DEBUG: c2s undo like request arrived in outbox')
  404. messageId = messageJson['object']['object'].replace('/activity', '')
  405. if ':' in domain:
  406. domain = domain.split(':')[0]
  407. postFilename = locatePost(baseDir, nickname, domain, messageId)
  408. if not postFilename:
  409. if debug:
  410. print('DEBUG: c2s undo like post not found in inbox or outbox')
  411. print(messageId)
  412. return True
  413. undoLikesCollectionEntry(recentPostsCache, baseDir, postFilename,
  414. messageId, messageJson['actor'],
  415. domain, debug)
  416. if debug:
  417. print('DEBUG: post undo liked via c2s - '+postFilename)