activities.py 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201
  1. """
  2. ForgeFed plugin for Pagure.
  3. Copyright (C) 2020-2021 zPlus <zplus@peers.community>
  4. This program is free software; you can redistribute it and/or modify
  5. it under the terms of the GNU General Public License as published by
  6. the Free Software Foundation; either version 2 of the License, or
  7. (at your option) any later version.
  8. This program is distributed in the hope that it will be useful,
  9. but WITHOUT ANY WARRANTY; without even the implied warranty of
  10. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  11. GNU General Public License for more details.
  12. You should have received a copy of the GNU General Public License along
  13. with this program; if not, see <https://www.gnu.org/licenses/>.
  14. SPDX-FileCopyrightText: 2020-2021 zPlus <zplus@peers.community>
  15. SPDX-License-Identifier: GPL-2.0-only
  16. """
  17. import celery
  18. import json
  19. import pagure.lib.query
  20. import rdflib
  21. import re
  22. import requests
  23. import requests_http_signature
  24. from .. import APP_URL
  25. from .. import activitypub
  26. from .. import feeds
  27. from .. import model
  28. from .. import settings
  29. from . import broker_url
  30. from . import broker
  31. from . import database_session
  32. log = celery.utils.log.get_task_logger(__name__)
  33. log.setLevel(settings.LOG_LEVEL)
  34. # The following is a decorator that accepts a dictionary as input and adds it to
  35. # _PATTERNS_ together with the decorated function. The dictionary is used as a
  36. # pattern for matching incoming Activities. If an incoming Activity matches one
  37. # of the patters in _PATTERNS_, then the corresponding function is executed.
  38. _PATTERNS_ = []
  39. def pattern(activity_pattern):
  40. def closure(func):
  41. def decorator(*args, **kwargs):
  42. func(*args, **kwargs)
  43. global _PATTERNS_
  44. _PATTERNS_.append((activity_pattern, decorator))
  45. return decorator
  46. return closure
  47. # The firs one is used to match *any* value in the Activity, or in other words to
  48. # make sure that the key exists irregardless of its value.
  49. # The other two are used to match a local or remote object only
  50. _ = activitypub.Document.AnyType()
  51. _remote = re.compile('^(?!'+APP_URL+')')
  52. _local = re.compile('^'+APP_URL)
  53. @broker.task
  54. def perform(activity):
  55. """
  56. This task is responsible for accepting an incoming Activity after it's been
  57. validated (see tasks.delivery) and decide what to do with it.
  58. :param activity: the Activity that was sent to the Actor
  59. """
  60. activity = activitypub.Activity.from_dict(activity)
  61. for activity_pattern, function in _PATTERNS_:
  62. if activity.match(activity_pattern):
  63. with database_session() as database:
  64. return function(database, activity)
  65. log.debug('Activity did not match any pattern. Ignoring incoming Activity: {}'.format(
  66. json.dumps(activity['id'], indent=4, sort_keys=True)))
  67. # Repository
  68. # -----------------------------------------------------------------------------
  69. @pattern({
  70. 'type': 'Create',
  71. 'id': _,
  72. 'actor': {
  73. 'id': _
  74. },
  75. 'object': {
  76. 'type': 'TagRef',
  77. 'id': _,
  78. 'name': _,
  79. 'context': {
  80. 'type': 'Repository',
  81. 'id': _
  82. }
  83. },
  84. })
  85. def create_repository_tag(database, activity):
  86. """
  87. Somebody has Created a Tag reference in a repository.
  88. """
  89. log.debug('Handling incoming Create(TagRef) Activity.')
  90. tagref = activity['object']
  91. repository = tagref['context']
  92. # Get the list of Actors that are Following the Repository
  93. items = database \
  94. .query(model.Collection) \
  95. .filter(model.Collection.uri == repository['followers']) \
  96. .all()
  97. # Only the local Actor
  98. followers = [ result.item for result in items if result.item.startswith(APP_URL) ]
  99. # Add a feed to recipients
  100. for follower in followers:
  101. actor = model.from_uri(database, follower)
  102. if not actor:
  103. continue
  104. database.add(model.Feed(actor.uri, feeds.create_tagref(repository, tagref)))
  105. # Follow
  106. # -----------------------------------------------------------------------------
  107. @pattern({
  108. 'type': 'Follow',
  109. 'id': _,
  110. 'actor': {
  111. 'id': _
  112. },
  113. 'object': {
  114. 'id': _
  115. },
  116. })
  117. def follow(database, activity):
  118. """
  119. An Actor has Followed another Actor.
  120. """
  121. log.debug('Handling incoming Follow Activity.')
  122. followed = model.from_uri(database, activity['object']['id'])
  123. if not followed:
  124. log.debug('Followed Actor {} does not exist on this instance.'.format(followed.uri))
  125. return
  126. if followed.is_remote:
  127. log.debug('Ignoring Follow Activity {} received for a remote Actor {}'.format(
  128. activity['id'], activity['object']['id']))
  129. return
  130. # Check if the local actor is already following the remote actor
  131. if database.query(
  132. database.query(model.Collection) \
  133. .filter(model.Collection.uri == activity['object']['following']) \
  134. .filter(model.Collection.item == activity['actor']['id']) \
  135. .exists()
  136. ).scalar():
  137. log.info('Actor {} is already following {}. Ignoring Follow request.'.format(followed.uri, follower.uri))
  138. return
  139. # Automatically Accept any Follow request
  140. if followed.is_local:
  141. # Update the followers/following collections
  142. database.merge(model.Collection(
  143. uri = activity['object']['following'],
  144. item = activity['actor']['id']))
  145. database.merge(model.Collection(
  146. uri = activity['actor']['following'],
  147. item = activity['object']['id']))
  148. database.merge(model.Collection(
  149. uri = activity['object']['followers'],
  150. item = activity['actor']['id']))
  151. database.merge(model.Collection(
  152. uri = activity['actor']['followers'],
  153. item = activity['object']['id']))
  154. # Send the Accept Activity
  155. activitypub.Activity(
  156. type = 'Accept',
  157. actor = followed.uri,
  158. object = activity['id']
  159. ).distribute()
  160. # Cache the remote Actor
  161. database.merge(model.Resource(
  162. uri = activity['actor']['id'],
  163. document = json.dumps(activity['actor'])))
  164. # Add a feed
  165. database.add(model.Feed(followed.uri, feeds.follow(activity['actor'], activity['object'])))
  166. database.add(model.Feed(followed.uri, feeds.accept_follow(activity['actor'], activity['object'])))
  167. @pattern({
  168. 'type': 'Accept',
  169. 'id': _,
  170. 'actor': {
  171. 'id': _
  172. },
  173. 'object': {
  174. 'type': 'Follow',
  175. 'id': _,
  176. 'actor': {
  177. 'id': _
  178. },
  179. 'object': {
  180. 'id': _
  181. }
  182. },
  183. })
  184. def accept_follow(database, activity):
  185. """
  186. Somebody has Accepted of a Follow request.
  187. """
  188. log.debug('Handling incoming Accept(Follow) Activity.')
  189. follow = activity['object']
  190. if follow['object']['id'] != activity['actor']['id']:
  191. log.debug('Only target Actor can Accept a Follow request.\n{}'.format(json.dumps(activity, indent=4)))
  192. return
  193. log.debug('{} has accepted Follow request from {}'.format(
  194. follow['object']['id'], follow['actor']['id']))
  195. # We cache a copy of the Actors for quick lookups. This is used for
  196. # example when listing "following/followers" collections. If we only
  197. # stored the actors' URI we would need to GET the JSON data every time
  198. # for getting its name.
  199. database.merge(model.Resource(
  200. uri = follow['actor']['id'],
  201. document = json.dumps(activity['actor'])))
  202. database.merge(model.Resource(
  203. uri = follow['object']['id'],
  204. document = json.dumps(follow['object'])))
  205. # Update the followers/following collections
  206. database.merge(model.Collection(
  207. uri = follow['actor']['following'],
  208. item = follow['object']['id']))
  209. database.merge(model.Collection(
  210. uri = follow['object']['following'],
  211. item = follow['actor']['id']))
  212. database.merge(model.Collection(
  213. uri = follow['actor']['followers'],
  214. item = follow['object']['id']))
  215. database.merge(model.Collection(
  216. uri = follow['object']['followers'],
  217. item = follow['actor']['id']))
  218. # If the follower is local
  219. if follow['actor']['id'].startswith(APP_URL):
  220. database.add(model.Feed(follow['actor']['id'],
  221. feeds.accept_follow(follow['actor'], follow['object'])))
  222. # Ticket
  223. # -----------------------------------------------------------------------------
  224. @pattern({
  225. 'type': 'Create',
  226. 'id': _,
  227. 'actor': {
  228. 'id': _
  229. },
  230. 'object': {
  231. 'type': 'Ticket',
  232. 'id': _,
  233. 'context': _,
  234. 'attributedTo': _,
  235. 'summary': _,
  236. 'content': _
  237. },
  238. })
  239. def create_ticket(database, activity):
  240. """
  241. Remote user has created a new Ticket for one of our TicketTracker.
  242. """
  243. log.debug('Handling incoming Create(Ticket) Activity.')
  244. ticket = activity['object']
  245. ticket_tracker = model.from_uri(database, ticket['context'])
  246. if not ticket_tracker:
  247. log.debug('Received a new Ticket for nonexistent TicketTracker {}'.format(ticket['context']))
  248. return
  249. log.debug('Request to create a new Ticket.')
  250. # Make sure we have a local actor representing the remote actor
  251. actor = model.Person.test_or_set(database, ticket['attributedTo'])
  252. # Create a new local issue in Pagure
  253. issue = pagure.lib.query.new_issue(
  254. database,
  255. ticket_tracker,
  256. ticket['summary'],
  257. ticket['content'],
  258. actor.username)
  259. if ticket_tracker.is_local:
  260. # Send an "Accept" request to notify the Actor that the Ticket was
  261. # Accepted (ticket are accepted automatically)
  262. activitypub.Activity(
  263. type = 'Accept',
  264. actor = ticket_tracker.local_uri,
  265. object = activity['id'],
  266. result = issue.uri
  267. ).distribute()
  268. else:
  269. # If it's a remote issue, we just need to keep track of it (we don't
  270. # need to Accept anything)
  271. issue.remote_uri = ticket['id']
  272. @pattern({
  273. 'type': 'Accept',
  274. 'id': _,
  275. 'actor': {
  276. 'id': _
  277. },
  278. 'object': {
  279. 'type': 'Create',
  280. 'id': _,
  281. 'actor': {},
  282. 'object': {
  283. 'type': 'Ticket',
  284. 'id': _,
  285. 'context': _,
  286. 'attributedTo': _,
  287. 'summary': _,
  288. 'content': _
  289. }
  290. },
  291. 'result': _
  292. })
  293. def accept_ticket(database, activity):
  294. """
  295. A Ticket was Accepted.
  296. """
  297. log.debug('Handling incoming Accept(Create(Ticket)) Activity.')
  298. ticket = activity['object']['object']
  299. database.add(model.SameAs(
  300. local_uri = ticket['id'],
  301. remote_uri = activity['result']
  302. ))
  303. database.add(model.Feed(ticket['attributedTo'], feeds.accept_ticket(activity['actor'], ticket)))
  304. @pattern({
  305. 'type': 'Update',
  306. 'id': _,
  307. 'actor': {
  308. 'id': _
  309. },
  310. 'object': {
  311. 'type': 'Ticket',
  312. 'id': _local
  313. },
  314. 'result': {}
  315. })
  316. def update_local_ticket(database, activity):
  317. """
  318. Remote user has updated a Ticket belonging to this instance.
  319. """
  320. log.debug('Handling incoming Update(Ticket) Activity.')
  321. remote_ticket = activity['object']
  322. ticket = model.from_uri(database, remote_ticket['id'])
  323. if not ticket:
  324. log.debug('Ticket does not exist: {}'.format(remote_ticket['id']))
  325. return
  326. actor = model.Person.test_or_set(database, activity['actor']['id'])
  327. new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
  328. new_content = activity['result']['content'] if 'content' in activity['result'] else None
  329. # Edit the pagure Issue object
  330. pagure.lib.query.edit_issue(
  331. session=database,
  332. issue=ticket,
  333. user=actor.username,
  334. title=new_title,
  335. content=new_content)
  336. log.debug('Ticket updated: {}'.format(activity['object']['id']))
  337. @pattern({
  338. 'type': 'Update',
  339. 'id': _,
  340. 'actor': {
  341. 'id': _
  342. },
  343. 'object': {
  344. 'type': 'Ticket',
  345. 'id': _remote
  346. },
  347. 'result': {}
  348. })
  349. def update_remote_ticket(database, activity):
  350. """
  351. Remote user has updated a remote Ticket not belonging to this instance.
  352. """
  353. log.debug('Handling incoming Update(Ticket) Activity.')
  354. remote_ticket = activity['object']
  355. ticket = model.from_uri(database, remote_ticket['id'])
  356. if not ticket:
  357. log.debug('Ticket {} does not exist on this instance.'.format(remote_ticket['id']))
  358. return
  359. actor = model.Person.test_or_set(database, activity['actor']['id'])
  360. new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
  361. new_content = activity['result']['content'] if 'content' in activity['result'] else None
  362. # Edit the pagure Issue object
  363. pagure.lib.query.edit_issue(
  364. session=database,
  365. issue=ticket,
  366. user=actor.username,
  367. title=new_title,
  368. content=new_content)
  369. log.debug('Ticket updated: {}'.format(activity['object']['id']))
  370. @pattern({
  371. 'type': 'Create',
  372. 'id': _,
  373. 'actor': {
  374. 'id': _
  375. },
  376. 'object': {
  377. 'type': 'Note',
  378. 'id': _remote,
  379. 'context': {
  380. 'id': _,
  381. 'type': 'Ticket'
  382. },
  383. 'attributedTo': {
  384. 'id': _
  385. },
  386. 'content': _
  387. },
  388. })
  389. def create_ticket_comment(database, activity):
  390. """
  391. Remote user has created a new TicketComment for one of our TicketTracker.
  392. """
  393. log.debug('Handling incoming Create(Note(Ticket)) Activity.')
  394. note = activity['object']
  395. # Check if we already have a local object for this remote comment
  396. if database.query(
  397. database.query(model.SameAs)
  398. .filter(model.SameAs.remote_uri == note['id']).exists()
  399. ).scalar():
  400. log.debug('The note {} is already stored in the database. Will not create a new one.'.format(note['id']))
  401. return
  402. author = model.Person.test_or_set(database, note['attributedTo']['id'])
  403. # Check if there is any local Ticket in the pagure database that is used
  404. # to track this Ticket
  405. ticket = model.from_uri(database, note['context']['id'])
  406. if not ticket:
  407. log.debug('No local Ticket for {}. Ignoring Note.'.format(note['context']['id']))
  408. return
  409. # Create the new comment to the ticket
  410. pagure.lib.query.add_issue_comment(
  411. session = database,
  412. issue = ticket,
  413. comment = note['content'],
  414. user = author.username)
  415. @pattern({
  416. 'type': 'Resolve',
  417. 'id': _,
  418. 'actor': {
  419. 'id': _
  420. },
  421. 'object': {
  422. 'type': 'Ticket',
  423. 'id': _local
  424. },
  425. })
  426. def resolve_local_ticket(database, activity):
  427. """
  428. Remote user has closed a Ticket belonging to this instance.
  429. """
  430. log.debug('Handling incoming Resolve(Ticket) Activity.')
  431. ticket = model.from_uri(database, activity['object']['id'])
  432. if not ticket:
  433. log.debug('Ticket does not exist: {}'.format(activity['object']['id']))
  434. return
  435. actor = model.Person.test_or_set(database, activity['actor']['id'])
  436. # Edit the pagure Issue object
  437. pagure.lib.query.edit_issue(
  438. session=database,
  439. issue=ticket,
  440. user=actor.username,
  441. status='Closed')
  442. log.debug('Ticket resolved: {}'.format(activity['object']['id']))
  443. @pattern({
  444. 'type': 'Resolve',
  445. 'id': _,
  446. 'actor': {
  447. 'id': _
  448. },
  449. 'object': {
  450. 'type': 'Ticket',
  451. 'id': _remote
  452. },
  453. })
  454. def resolve_remote_ticket(database, activity):
  455. """
  456. Remote user has closed a remote Ticket not belonging to this instance.
  457. """
  458. log.debug('Handling incoming Resolve(Ticket) Activity.')
  459. ticket = model.from_uri(database, activity['object']['id'])
  460. # Nothing to do because we don't have any local TicketTracker tracking
  461. # this remote Ticket
  462. if not ticket:
  463. log.debug('Untracked Ticket resolved: {}'.format(activity['object']['id']))
  464. return
  465. actor = model.Person.test_or_set(database, activity['actor']['id'])
  466. # Edit the pagure Issue object
  467. pagure.lib.query.edit_issue(
  468. session=database,
  469. issue=ticket,
  470. user=actor.username,
  471. status='Closed')
  472. log.debug('Ticket resolved: {}'.format(activity['object']['id']))
  473. @pattern({
  474. 'type': 'Reopen',
  475. 'id': _,
  476. 'actor': {
  477. 'id': _
  478. },
  479. 'object': {
  480. 'type': 'Ticket',
  481. 'id': _local
  482. },
  483. })
  484. def reopen_local_ticket(database, activity):
  485. """
  486. Remote user has reopened a Ticket belonging to this instance.
  487. """
  488. log.debug('Handling incoming Reopen(Ticket) Activity.')
  489. ticket = model.from_uri(database, activity['object']['id'])
  490. if not ticket:
  491. log.debug('Ticket does not exist: {}'.format(activity['object']['id']))
  492. return
  493. actor = model.Person.test_or_set(database, activity['actor']['id'])
  494. # Edit the pagure Issue object
  495. pagure.lib.query.edit_issue(
  496. session=database,
  497. issue=ticket,
  498. user=actor.username,
  499. status='Open')
  500. log.debug('Ticket reopened: {}'.format(activity['object']['id']))
  501. @pattern({
  502. 'type': 'Reopen',
  503. 'id': _,
  504. 'actor': {
  505. 'id': _
  506. },
  507. 'object': {
  508. 'type': 'Ticket',
  509. 'id': _remote
  510. },
  511. })
  512. def reopen_remote_ticket(database, activity):
  513. """
  514. Remote user has reopened a remote Ticket not belonging to this instance.
  515. """
  516. log.debug('Handling incoming Reopen(Ticket) Activity.')
  517. ticket = model.from_uri(database, activity['object']['id'])
  518. # Nothing to do because we don't have any local TicketTracker tracking
  519. # this remote Ticket
  520. if not ticket:
  521. log.debug('Untracked Ticket reopened: {}'.format(activity['object']['id']))
  522. return
  523. actor = model.Person.test_or_set(database, activity['actor']['id'])
  524. # Edit the pagure Issue object
  525. pagure.lib.query.edit_issue(
  526. session=database,
  527. issue=ticket,
  528. user=actor.username,
  529. status='Open')
  530. log.debug('Ticket reopened: {}'.format(activity['object']['id']))
  531. @pattern({
  532. 'type': 'Delete',
  533. 'id': _,
  534. 'actor': {
  535. 'id': _
  536. },
  537. 'object': _,
  538. 'origin': {
  539. 'id': _,
  540. 'type': 'TicketTracker'
  541. }
  542. })
  543. def delete_ticket(database, activity):
  544. """
  545. Remote user has deleted a remote Ticket not belonging to this instance.
  546. """
  547. log.debug('Handling incoming Delete(Ticket) Activity.')
  548. object = activity['object']
  549. if object.startswith(APP_URL):
  550. log.debug('Actor not allowed to delete local Ticket {}'.format(ticket.uri))
  551. return
  552. # The Actor that has deleted the Ticket
  553. activity_actor = model.Person.test_or_set(database, activity['actor']['id'])
  554. # Retrieve the local URI of the object
  555. same_as = database \
  556. .query(model.SameAs) \
  557. .filter(model.SameAs.remote_uri == object) \
  558. .all()
  559. uri_list = [ row.local_uri for row in same_as ]
  560. # Delete all remote_uri from the SameAs table
  561. database \
  562. .query(model.SameAs) \
  563. .filter(model.SameAs.remote_uri == object) \
  564. .delete()
  565. for local_uri in uri_list:
  566. ticket = model.from_uri(database, local_uri)
  567. if not ticket:
  568. continue
  569. pagure.lib.query.drop_issue(database, ticket, activity_actor.username)
  570. log.debug('Deleted Ticket {}'.format(local_uri))
  571. # MergeRequest
  572. # -----------------------------------------------------------------------------
  573. @pattern({
  574. 'type': 'Create',
  575. 'id': _,
  576. 'actor': {
  577. 'id': _
  578. },
  579. 'object': {
  580. 'type': 'MergeRequest',
  581. 'id': _,
  582. 'attributedTo': _,
  583. 'upstream': {
  584. 'branch': _,
  585. 'repository': {
  586. 'id': _local,
  587. 'type': 'Repository'
  588. }
  589. },
  590. 'downstream': {
  591. 'branch': _,
  592. 'repository': {
  593. 'id': _,
  594. 'type': 'Repository'
  595. }
  596. },
  597. 'summary': _,
  598. 'content': _,
  599. 'commit_start': _,
  600. 'commit_stop': _,
  601. },
  602. })
  603. def create_merge_request(database, activity):
  604. """
  605. Remote user has created a new MergeRequest for one of our Repositories.
  606. """
  607. log.debug('Handling incoming Create(MergeRequest) Activity.')
  608. mergerequest = activity['object']
  609. repository = model.from_uri(database, mergerequest['upstream']['repository']['id'])
  610. if not repository:
  611. log.debug('Received a new MergeRequest for nonexistent Repository {}'.format(mergerequest['upstream']['id']))
  612. return
  613. log.debug('Request to create a new MergeRequest.')
  614. # First of all, let's pull down the remote repository (synchronously)
  615. pagure.lib.tasks.pull_remote_repo.apply(kwargs={
  616. 'remote_git': mergerequest['downstream']['repository']['id'],
  617. 'branch_from': mergerequest['downstream']['branch']
  618. })
  619. # Make sure we have a local actor representing the remote actor
  620. actor = model.Person.test_or_set(database, mergerequest['attributedTo'])
  621. # Create a new local PullRequest in Pagure
  622. mergerequest = pagure.lib.query.new_pull_request(
  623. session=database,
  624. branch_from=mergerequest['downstream']['branch'],
  625. repo_to=repository,
  626. branch_to=mergerequest['upstream']['branch'],
  627. title=mergerequest['summary'],
  628. user=actor.username,
  629. initial_comment=mergerequest['content'],
  630. repo_from=None,
  631. remote_git=mergerequest['downstream']['repository']['id'],
  632. commit_start=mergerequest['commit_start'],
  633. commit_stop=mergerequest['commit_stop'])
  634. # Send an "Accept" request to notify the remote actor that the
  635. # MergeRequest was accepted
  636. activitypub.Activity(
  637. type = 'Accept',
  638. actor = repository.local_uri,
  639. object = activity['id'],
  640. result = mergerequest.uri
  641. ).distribute()
  642. @pattern({
  643. 'type': 'Accept',
  644. 'id': _,
  645. 'actor': {
  646. 'id': _
  647. },
  648. 'object': {
  649. 'type': 'Create',
  650. 'id': _,
  651. 'actor': {
  652. 'id': _
  653. },
  654. 'object': {
  655. 'type': 'MergeRequest',
  656. 'id': _,
  657. 'attributedTo': _,
  658. 'upstream': {
  659. 'branch': _,
  660. 'repository': {
  661. 'id': _,
  662. 'type': 'Repository'
  663. }
  664. },
  665. 'downstream': {
  666. 'branch': _,
  667. 'repository': {
  668. 'id': _local,
  669. 'type': 'Repository'
  670. }
  671. },
  672. },
  673. },
  674. 'result': _
  675. })
  676. def accept_merge_request(database, activity):
  677. """
  678. A MergeRequest was Accepted.
  679. """
  680. log.debug('Handling incoming Accept(Create(MergeRequest)) Activity.')
  681. mergerequest = activity['object']['object']
  682. database.add(model.SameAs(
  683. local_uri = mergerequest['id'],
  684. remote_uri = activity['result']
  685. ))
  686. database.add(model.Feed(mergerequest['attributedTo'],
  687. feeds.accept_mergerequest(mergerequest)))
  688. @pattern({
  689. 'type': 'Update',
  690. 'id': _,
  691. 'actor': {
  692. 'id': _
  693. },
  694. 'object': {
  695. 'type': 'MergeRequest',
  696. 'id': _local
  697. },
  698. 'result': {}
  699. })
  700. def update_local_mergerequest(database, activity):
  701. """
  702. Remote user has updated a MergeRequest belonging to this instance.
  703. """
  704. log.debug('Handling incoming Update(MergeRequest) Activity.')
  705. mergerequest_jsonld = activity['object']
  706. mergerequest = model.from_uri(database, mergerequest_jsonld['id'])
  707. if not mergerequest:
  708. log.debug('MergeRequest does not exist: {}'.format(mergerequest_jsonld['id']))
  709. return
  710. actor = model.Person.test_or_set(database, activity['actor']['id'])
  711. new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
  712. new_content = activity['result']['content'] if 'content' in activity['result'] else None
  713. # Edit the pagure PullRequest object
  714. mergerequest.title = new_title
  715. mergerequest.initial_comment = new_content
  716. log.debug('MergeRequest updated: {}'.format(mergerequest_jsonld['id']))
  717. @pattern({
  718. 'type': 'Update',
  719. 'id': _,
  720. 'actor': {
  721. 'id': _
  722. },
  723. 'object': {
  724. 'type': 'MergeRequest',
  725. 'id': _remote
  726. },
  727. 'result': {}
  728. })
  729. def update_remote_mergerequest(database, activity):
  730. """
  731. Remote user has updated a remote MergeRequest not belonging to this instance.
  732. """
  733. log.debug('Handling incoming Update(MergeRequest) Activity.')
  734. mergerequest_jsonld = activity['object']
  735. mergerequest = model.from_uri(database, mergerequest_jsonld['id'])
  736. if not mergerequest:
  737. log.debug('MergeRequest does not exist: {}'.format(mergerequest_jsonld['id']))
  738. return
  739. actor = model.Person.test_or_set(database, activity['actor']['id'])
  740. new_title = activity['result']['summary'] if 'summary' in activity['result'] else None
  741. new_content = activity['result']['content'] if 'content' in activity['result'] else None
  742. # Edit the pagure PullRequest object
  743. mergerequest.title = new_title
  744. mergerequest.initial_comment = new_content
  745. log.debug('MergeRequest updated: {}'.format(mergerequest_jsonld['id']))
  746. @pattern({
  747. 'type': 'Resolve',
  748. 'id': _,
  749. 'actor': {
  750. 'id': _
  751. },
  752. 'object': {
  753. 'type': 'MergeRequest',
  754. 'id': _local
  755. },
  756. })
  757. def merge_local_mergerequest(database, activity):
  758. """
  759. Remote user has merged a MergeRequest belonging to this instance.
  760. """
  761. pass
  762. @pattern({
  763. 'type': 'Resolve',
  764. 'id': _,
  765. 'actor': {
  766. 'id': _
  767. },
  768. 'object': {
  769. 'type': 'MergeRequest',
  770. 'id': _remote,
  771. },
  772. })
  773. def merge_remote_mergerequest(database, activity):
  774. """
  775. Remote user has merged a remote MergeRequest not belonging to this instance.
  776. """
  777. log.debug('Handling incoming Resolve(MergeRequest) Activity.')
  778. mergerequest = model.from_uri(database, activity['object']['id'])
  779. # Nothing to do because we don't have any local MergeRequest tracking
  780. # this remote object
  781. if not mergerequest:
  782. log.debug('Untracked MergeRequest has been merged: {}'.format(activity['object']['id']))
  783. return
  784. actor = model.Person.test_or_set(database, activity['actor']['id'])
  785. # Edit the pagure PullRequest object
  786. pagure.lib.query.close_pull_request(
  787. session=database,
  788. request=mergerequest,
  789. user=actor.username,
  790. merged=True)
  791. log.debug('MergeRequest merged: {}'.format(activity['object']['id']))
  792. @pattern({
  793. 'type': 'Close',
  794. 'id': _,
  795. 'actor': {
  796. 'id': _remote
  797. },
  798. 'object': {
  799. 'type': 'MergeRequest',
  800. 'id': _local
  801. },
  802. })
  803. def close_local_mergerequest(database, activity):
  804. """
  805. Remote user has closed a MergeRequest belonging to this instance.
  806. """
  807. log.debug('Handling incoming Close(MergeRequest) Activity.')
  808. mergerequest = model.from_uri(database, activity['object']['id'])
  809. # Nothing to do because we don't have any local MergeRequest with this URI
  810. if not mergerequest:
  811. log.debug('Remote user {} tried to close nonexistent MergeRequest {}'.format(activity['actor']['id'], activity['object']['id']))
  812. return
  813. actor = model.Person.test_or_set(database, activity['actor']['id'])
  814. # Check if the remote actor has the right to close the MergeRequest
  815. if mergerequest.user.remote_uri != actor.remote_uri:
  816. log.debug('Remote actor {} cannot close local MergeRequest {}'.format(actor.remote_uri, mergerequest.local_uri))
  817. return
  818. # Edit the pagure PullRequest object
  819. pagure.lib.query.close_pull_request(
  820. session=database,
  821. request=mergerequest,
  822. user=actor.username,
  823. merged=False)
  824. log.debug('MergeRequest closed: {}'.format(activity['object']['id']))
  825. @pattern({
  826. 'type': 'Close',
  827. 'id': _,
  828. 'actor': {
  829. 'id': _remote
  830. },
  831. 'object': {
  832. 'type': 'MergeRequest',
  833. 'id': _remote
  834. },
  835. })
  836. def close_remote_mergerequest(database, activity):
  837. """
  838. Remote user has closed a remote MergeRequest not belonging to this instance.
  839. """
  840. log.debug('Handling incoming Close(MergeRequest) Activity.')
  841. mergerequest = model.from_uri(database, activity['object']['id'])
  842. # Nothing to do because we don't have any local MergeRequest tracking
  843. # this remote object
  844. if not mergerequest:
  845. log.debug('Untracked MergeRequest has been closed: {}'.format(activity['object']['id']))
  846. return
  847. actor = model.Person.test_or_set(database, activity['actor']['id'])
  848. # Edit the pagure PullRequest object
  849. pagure.lib.query.close_pull_request(
  850. session=database,
  851. request=mergerequest,
  852. user=actor.username,
  853. merged=False)
  854. log.debug('MergeRequest closed: {}'.format(activity['object']['id']))
  855. @pattern({
  856. 'type': 'Reopen',
  857. 'id': _,
  858. 'actor': {
  859. 'id': _remote,
  860. },
  861. 'object': {
  862. 'type': 'MergeRequest',
  863. 'id': _local
  864. },
  865. })
  866. def reopen_local_mergerequest(database, activity):
  867. """
  868. Remote user has reopened a MergeRequest belonging to this instance.
  869. """
  870. log.debug('Handling incoming Reopen(MergeRequest) Activity.')
  871. mergerequest = model.from_uri(database, activity['object']['id'])
  872. # Nothing to do because we don't have any local MergeRequest with this URI
  873. if not mergerequest:
  874. log.debug('Remote user {} tried to reopen nonexistent MergeRequest {}'.format(activity['actor']['id'], activity['object']['id']))
  875. return
  876. actor = model.Person.test_or_set(database, activity['actor']['id'])
  877. # Check if the remote actor has the right to reopen the MergeRequest
  878. if mergerequest.user.remote_uri != actor.remote_uri:
  879. log.debug('Remote actor {} cannot reopen local MergeRequest {}'.format(actor.remote_uri, mergerequest.local_uri))
  880. return
  881. # Edit the pagure PullRequest object
  882. pagure.lib.query.reopen_pull_request(
  883. session=database,
  884. request=mergerequest,
  885. user=actor.username)
  886. log.debug('MergeRequest reopened: {}'.format(activity['object']['id']))
  887. @pattern({
  888. 'type': 'Reopen',
  889. 'id': _,
  890. 'actor': {
  891. 'id': _remote
  892. },
  893. 'object': {
  894. 'type': 'MergeRequest',
  895. 'id': _remote
  896. },
  897. })
  898. def reopen_remote_mergerequest(database, activity):
  899. """
  900. Remote user has reopened a remote MergeRequest not belonging to this instance.
  901. """
  902. log.debug('Handling incoming Reopen(MergeRequest) Activity.')
  903. mergerequest = model.from_uri(database, activity['object']['id'])
  904. # Nothing to do because we don't have any local MergeRequest tracking
  905. # this remote object
  906. if not mergerequest:
  907. log.debug('Untracked MergeRequest has been reopened: {}'.format(activity['object']['id']))
  908. return
  909. actor = model.Person.test_or_set(database, activity['actor']['id'])
  910. # Edit the pagure PullRequest object
  911. pagure.lib.query.reopen_pull_request(
  912. session=database,
  913. request=mergerequest,
  914. user=actor.username)
  915. log.debug('MergeRequest reopened: {}'.format(activity['object']['id']))
  916. @pattern({
  917. 'type': 'Create',
  918. 'id': _,
  919. 'actor': {
  920. 'id': _
  921. },
  922. 'object': {
  923. 'type': 'Note',
  924. 'id': _remote,
  925. 'context': {
  926. 'type': 'MergeRequest',
  927. 'id': _
  928. },
  929. 'attributedTo': {
  930. 'id': _
  931. },
  932. 'content': _
  933. },
  934. })
  935. def create_mergerequest_comment(database, activity):
  936. """
  937. Remote user has created a new TicketComment for one of our TicketTracker.
  938. """
  939. log.debug('Handling incoming Create(Note(MergeRequest)) Activity.')
  940. note = activity['object']
  941. # Check if we already have a local object for this remote comment
  942. if database.query(
  943. database.query(model.SameAs)
  944. .filter(model.SameAs.remote_uri == note['id']).exists()
  945. ).scalar():
  946. log.debug('The note {} is already stored in the database. Will not create a new one.'.format(note['id']))
  947. return
  948. author = model.Person.test_or_set(database, note['attributedTo']['id'])
  949. # If the "context" of the Note is a local URL, it means somebody has created a
  950. # Note for a Repository in our instance. This is the case for example when
  951. # a remote user is contributing a comment to a local project.
  952. if note['context']['id'].startswith(APP_URL):
  953. mergerequest = model.from_uri(database, note['context']['id'])
  954. # If the "context" of the Note is not a local Ticket, this note was created
  955. # for a remote object. This is the case for example when a user has sent a
  956. # comment to a remote ticket, and we have received the Activity notification
  957. # because we are following that ticket.
  958. else:
  959. # Check if there is any local Ticket in the pagure database that is used
  960. # to track this remote Ticket
  961. mergerequest = model.from_uri(database, note['context']['id'])
  962. # This is a new Note for a remote Ticket
  963. if not mergerequest:
  964. log.debug('No local MergeRequest for {}'.format(note['context']['id']))
  965. return
  966. # Create the new comment to the ticket
  967. pagure.lib.query.add_pull_request_comment(
  968. session=database,
  969. request=mergerequest,
  970. commit=None,
  971. tree_id=None,
  972. filename=None,
  973. row=None,
  974. comment=note['content'],
  975. user=author.username)