collavre 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (410) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +221 -0
  3. data/Rakefile +8 -0
  4. data/app/assets/stylesheets/collavre/actiontext.css +577 -0
  5. data/app/assets/stylesheets/collavre/activity_logs.css +99 -0
  6. data/app/assets/stylesheets/collavre/comments_popup.css +692 -0
  7. data/app/assets/stylesheets/collavre/creatives.css +559 -0
  8. data/app/assets/stylesheets/collavre/dark_mode.css +118 -0
  9. data/app/assets/stylesheets/collavre/mention_menu.css +43 -0
  10. data/app/assets/stylesheets/collavre/popup.css +160 -0
  11. data/app/assets/stylesheets/collavre/print.css +37 -0
  12. data/app/assets/stylesheets/collavre/slide_view.css +79 -0
  13. data/app/assets/stylesheets/collavre/user_menu.css +34 -0
  14. data/app/channels/collavre/comments_presence_channel.rb +54 -0
  15. data/app/channels/collavre/slide_view_channel.rb +11 -0
  16. data/app/channels/collavre/topics_channel.rb +12 -0
  17. data/app/components/collavre/avatar_component.html.erb +15 -0
  18. data/app/components/collavre/avatar_component.rb +59 -0
  19. data/app/components/collavre/inbox/badge_component.html.erb +6 -0
  20. data/app/components/collavre/inbox/badge_component.rb +18 -0
  21. data/app/components/collavre/plans_timeline_component.html.erb +14 -0
  22. data/app/components/collavre/plans_timeline_component.rb +56 -0
  23. data/app/components/collavre/popup_menu_component.html.erb +6 -0
  24. data/app/components/collavre/popup_menu_component.rb +30 -0
  25. data/app/components/collavre/progress_filter_component.html.erb +5 -0
  26. data/app/components/collavre/progress_filter_component.rb +10 -0
  27. data/app/components/collavre/user_mention_menu_component.html.erb +3 -0
  28. data/app/components/collavre/user_mention_menu_component.rb +8 -0
  29. data/app/controllers/collavre/application_controller.rb +15 -0
  30. data/app/controllers/collavre/attachments_controller.rb +44 -0
  31. data/app/controllers/collavre/calendar_events_controller.rb +15 -0
  32. data/app/controllers/collavre/comment_read_pointers_controller.rb +80 -0
  33. data/app/controllers/collavre/comments/activity_logs_controller.rb +26 -0
  34. data/app/controllers/collavre/comments/reactions_controller.rb +82 -0
  35. data/app/controllers/collavre/comments_controller.rb +464 -0
  36. data/app/controllers/collavre/contacts_controller.rb +10 -0
  37. data/app/controllers/collavre/creative_expanded_states_controller.rb +27 -0
  38. data/app/controllers/collavre/creative_imports_controller.rb +24 -0
  39. data/app/controllers/collavre/creative_plans_controller.rb +69 -0
  40. data/app/controllers/collavre/creative_shares_controller.rb +79 -0
  41. data/app/controllers/collavre/creatives_controller.rb +535 -0
  42. data/app/controllers/collavre/devices_controller.rb +19 -0
  43. data/app/controllers/collavre/email_verifications_controller.rb +16 -0
  44. data/app/controllers/collavre/emails_controller.rb +11 -0
  45. data/app/controllers/collavre/github_auth_controller.rb +25 -0
  46. data/app/controllers/collavre/google_auth_controller.rb +43 -0
  47. data/app/controllers/collavre/inbox_items_controller.rb +64 -0
  48. data/app/controllers/collavre/invites_controller.rb +27 -0
  49. data/app/controllers/collavre/notion_auth_controller.rb +25 -0
  50. data/app/controllers/collavre/passwords_controller.rb +37 -0
  51. data/app/controllers/collavre/plans_controller.rb +110 -0
  52. data/app/controllers/collavre/sessions_controller.rb +57 -0
  53. data/app/controllers/collavre/topics_controller.rb +58 -0
  54. data/app/controllers/collavre/user_themes_controller.rb +58 -0
  55. data/app/controllers/collavre/users_controller.rb +390 -0
  56. data/app/helpers/collavre/application_helper.rb +4 -0
  57. data/app/helpers/collavre/comments_helper.rb +9 -0
  58. data/app/helpers/collavre/creatives_helper.rb +343 -0
  59. data/app/helpers/collavre/navigation_helper.rb +163 -0
  60. data/app/helpers/collavre/user_themes_helper.rb +4 -0
  61. data/app/javascript/collavre.js +26 -0
  62. data/app/javascript/components/InlineLexicalEditor.jsx +889 -0
  63. data/app/javascript/components/LinkPopup.jsx +112 -0
  64. data/app/javascript/components/creative_tree_row.js +503 -0
  65. data/app/javascript/components/plugins/attachment_cleanup_plugin.jsx +95 -0
  66. data/app/javascript/components/plugins/image_upload_plugin.jsx +162 -0
  67. data/app/javascript/controllers/click_target_controller.js +13 -0
  68. data/app/javascript/controllers/comment_controller.js +162 -0
  69. data/app/javascript/controllers/comments/__tests__/popup_controller.test.js +68 -0
  70. data/app/javascript/controllers/comments/form_controller.js +530 -0
  71. data/app/javascript/controllers/comments/list_controller.js +715 -0
  72. data/app/javascript/controllers/comments/mention_menu_controller.js +41 -0
  73. data/app/javascript/controllers/comments/popup_controller.js +385 -0
  74. data/app/javascript/controllers/comments/presence_controller.js +311 -0
  75. data/app/javascript/controllers/comments/topics_controller.js +338 -0
  76. data/app/javascript/controllers/common_popup_controller.js +55 -0
  77. data/app/javascript/controllers/creatives/drag_drop_controller.js +45 -0
  78. data/app/javascript/controllers/creatives/expansion_controller.js +222 -0
  79. data/app/javascript/controllers/creatives/import_controller.js +116 -0
  80. data/app/javascript/controllers/creatives/row_editor_controller.js +8 -0
  81. data/app/javascript/controllers/creatives/select_mode_controller.js +231 -0
  82. data/app/javascript/controllers/creatives/set_plan_modal_controller.js +107 -0
  83. data/app/javascript/controllers/creatives/tree_controller.js +218 -0
  84. data/app/javascript/controllers/index.js +79 -0
  85. data/app/javascript/controllers/link_creative_controller.js +91 -0
  86. data/app/javascript/controllers/popup_menu_controller.js +82 -0
  87. data/app/javascript/controllers/progress_filter_controller.js +35 -0
  88. data/app/javascript/controllers/reaction_picker_controller.js +107 -0
  89. data/app/javascript/controllers/share_invite_controller.js +15 -0
  90. data/app/javascript/controllers/share_user_search_controller.js +121 -0
  91. data/app/javascript/controllers/tabs_controller.js +43 -0
  92. data/app/javascript/creatives/drag_drop/dom.js +170 -0
  93. data/app/javascript/creatives/drag_drop/event_handlers.js +846 -0
  94. data/app/javascript/creatives/drag_drop/indicator.js +35 -0
  95. data/app/javascript/creatives/drag_drop/operations.js +116 -0
  96. data/app/javascript/creatives/drag_drop/state.js +31 -0
  97. data/app/javascript/creatives/tree_renderer.js +248 -0
  98. data/app/javascript/lib/api/__tests__/queue_manager.test.js +153 -0
  99. data/app/javascript/lib/api/creatives.js +79 -0
  100. data/app/javascript/lib/api/csrf_fetch.js +22 -0
  101. data/app/javascript/lib/api/drag_drop.js +31 -0
  102. data/app/javascript/lib/api/queue_manager.js +423 -0
  103. data/app/javascript/lib/apply_lexical_styles.js +15 -0
  104. data/app/javascript/lib/common_popup.js +195 -0
  105. data/app/javascript/lib/lexical/__tests__/action_text_attachment_node.test.jsx +91 -0
  106. data/app/javascript/lib/lexical/__tests__/attachment_payload.test.js +194 -0
  107. data/app/javascript/lib/lexical/action_text_attachment_node.js +459 -0
  108. data/app/javascript/lib/lexical/attachment_node.jsx +170 -0
  109. data/app/javascript/lib/lexical/attachment_payload.js +293 -0
  110. data/app/javascript/lib/lexical/dom_attachment_utils.js +66 -0
  111. data/app/javascript/lib/lexical/image_node.jsx +159 -0
  112. data/app/javascript/lib/lexical/style_attributes.js +40 -0
  113. data/app/javascript/lib/responsive_images.js +54 -0
  114. data/app/javascript/lib/turbo_stream_actions.js +33 -0
  115. data/app/javascript/lib/utils/markdown.js +23 -0
  116. data/app/javascript/modules/creative_guide.js +53 -0
  117. data/app/javascript/modules/creative_row_editor.js +1841 -0
  118. data/app/javascript/modules/creative_row_swipe.js +43 -0
  119. data/app/javascript/modules/creatives.js +15 -0
  120. data/app/javascript/modules/export_to_markdown.js +34 -0
  121. data/app/javascript/modules/inbox_panel.js +226 -0
  122. data/app/javascript/modules/lexical_inline_editor.jsx +133 -0
  123. data/app/javascript/modules/mention_menu.js +77 -0
  124. data/app/javascript/modules/plans_menu.js +39 -0
  125. data/app/javascript/modules/plans_timeline.js +397 -0
  126. data/app/javascript/modules/share_modal.js +73 -0
  127. data/app/javascript/modules/share_user_popup.js +77 -0
  128. data/app/javascript/modules/slide_view.js +163 -0
  129. data/app/javascript/services/cable.js +32 -0
  130. data/app/javascript/slide_view.js +2 -0
  131. data/app/javascript/utils/caret_position.js +42 -0
  132. data/app/javascript/utils/clipboard.js +40 -0
  133. data/app/jobs/collavre/ai_agent_job.rb +27 -0
  134. data/app/jobs/collavre/inbox_summary_job.rb +24 -0
  135. data/app/jobs/collavre/notion_export_job.rb +30 -0
  136. data/app/jobs/collavre/notion_sync_job.rb +48 -0
  137. data/app/jobs/collavre/permission_cache_cleanup_job.rb +36 -0
  138. data/app/jobs/collavre/permission_cache_job.rb +71 -0
  139. data/app/jobs/collavre/push_notification_job.rb +86 -0
  140. data/app/mailers/collavre/application_mailer.rb +17 -0
  141. data/app/mailers/collavre/creative_mailer.rb +9 -0
  142. data/app/mailers/collavre/email_verification_mailer.rb +20 -0
  143. data/app/mailers/collavre/inbox_mailer.rb +19 -0
  144. data/app/mailers/collavre/invitation_mailer.rb +16 -0
  145. data/app/mailers/collavre/passwords_mailer.rb +10 -0
  146. data/app/models/collavre/activity_log.rb +13 -0
  147. data/app/models/collavre/application_record.rb +5 -0
  148. data/app/models/collavre/calendar_event.rb +20 -0
  149. data/app/models/collavre/comment.rb +307 -0
  150. data/app/models/collavre/comment_presence_store.rb +30 -0
  151. data/app/models/collavre/comment_reaction.rb +11 -0
  152. data/app/models/collavre/comment_read_pointer.rb +26 -0
  153. data/app/models/collavre/contact.rb +23 -0
  154. data/app/models/collavre/creative.rb +413 -0
  155. data/app/models/collavre/creative_expanded_state.rb +11 -0
  156. data/app/models/collavre/creative_share.rb +122 -0
  157. data/app/models/collavre/creative_shares_cache.rb +18 -0
  158. data/app/models/collavre/current.rb +14 -0
  159. data/app/models/collavre/device.rb +13 -0
  160. data/app/models/collavre/email.rb +14 -0
  161. data/app/models/collavre/github_account.rb +10 -0
  162. data/app/models/collavre/github_repository_link.rb +19 -0
  163. data/app/models/collavre/inbox_item.rb +95 -0
  164. data/app/models/collavre/invitation.rb +22 -0
  165. data/app/models/collavre/label.rb +47 -0
  166. data/app/models/collavre/mcp_tool.rb +30 -0
  167. data/app/models/collavre/notion_account.rb +17 -0
  168. data/app/models/collavre/notion_block_link.rb +10 -0
  169. data/app/models/collavre/notion_page_link.rb +19 -0
  170. data/app/models/collavre/plan.rb +20 -0
  171. data/app/models/collavre/session.rb +24 -0
  172. data/app/models/collavre/system_setting.rb +144 -0
  173. data/app/models/collavre/tag.rb +10 -0
  174. data/app/models/collavre/task.rb +10 -0
  175. data/app/models/collavre/task_action.rb +10 -0
  176. data/app/models/collavre/topic.rb +12 -0
  177. data/app/models/collavre/user.rb +174 -0
  178. data/app/models/collavre/user_theme.rb +10 -0
  179. data/app/models/collavre/variation.rb +5 -0
  180. data/app/models/collavre/webauthn_credential.rb +11 -0
  181. data/app/services/collavre/ai_agent_service.rb +193 -0
  182. data/app/services/collavre/ai_client.rb +183 -0
  183. data/app/services/collavre/ai_system_prompt_renderer.rb +38 -0
  184. data/app/services/collavre/auto_theme_generator.rb +198 -0
  185. data/app/services/collavre/comment_link_formatter.rb +60 -0
  186. data/app/services/collavre/comments/action_executor.rb +262 -0
  187. data/app/services/collavre/comments/action_validator.rb +58 -0
  188. data/app/services/collavre/comments/calendar_command.rb +97 -0
  189. data/app/services/collavre/comments/command_processor.rb +37 -0
  190. data/app/services/collavre/comments/mcp_command.rb +109 -0
  191. data/app/services/collavre/comments/mcp_command_builder.rb +32 -0
  192. data/app/services/collavre/creatives/filter_pipeline.rb +196 -0
  193. data/app/services/collavre/creatives/filters/assignee_filter.rb +30 -0
  194. data/app/services/collavre/creatives/filters/base_filter.rb +24 -0
  195. data/app/services/collavre/creatives/filters/comment_filter.rb +21 -0
  196. data/app/services/collavre/creatives/filters/date_filter.rb +58 -0
  197. data/app/services/collavre/creatives/filters/progress_filter.rb +25 -0
  198. data/app/services/collavre/creatives/filters/search_filter.rb +28 -0
  199. data/app/services/collavre/creatives/filters/tag_filter.rb +16 -0
  200. data/app/services/collavre/creatives/importer.rb +47 -0
  201. data/app/services/collavre/creatives/index_query.rb +191 -0
  202. data/app/services/collavre/creatives/path_exporter.rb +131 -0
  203. data/app/services/collavre/creatives/permission_cache_builder.rb +194 -0
  204. data/app/services/collavre/creatives/permission_checker.rb +42 -0
  205. data/app/services/collavre/creatives/plan_tagger.rb +53 -0
  206. data/app/services/collavre/creatives/progress_service.rb +89 -0
  207. data/app/services/collavre/creatives/reorderer.rb +187 -0
  208. data/app/services/collavre/creatives/tree_builder.rb +231 -0
  209. data/app/services/collavre/creatives/tree_formatter.rb +36 -0
  210. data/app/services/collavre/gemini_parent_recommender.rb +77 -0
  211. data/app/services/collavre/github/client.rb +112 -0
  212. data/app/services/collavre/github/pull_request_analyzer.rb +280 -0
  213. data/app/services/collavre/github/pull_request_processor.rb +181 -0
  214. data/app/services/collavre/github/webhook_provisioner.rb +130 -0
  215. data/app/services/collavre/google_calendar_service.rb +149 -0
  216. data/app/services/collavre/link_preview_fetcher.rb +230 -0
  217. data/app/services/collavre/markdown_importer.rb +202 -0
  218. data/app/services/collavre/mcp_service.rb +217 -0
  219. data/app/services/collavre/notion_client.rb +231 -0
  220. data/app/services/collavre/notion_creative_exporter.rb +296 -0
  221. data/app/services/collavre/notion_service.rb +249 -0
  222. data/app/services/collavre/ppt_importer.rb +76 -0
  223. data/app/services/collavre/ruby_llm_interaction_logger.rb +34 -0
  224. data/app/services/collavre/system_events/context_builder.rb +41 -0
  225. data/app/services/collavre/system_events/dispatcher.rb +19 -0
  226. data/app/services/collavre/system_events/router.rb +72 -0
  227. data/app/services/collavre/tools/creative_retrieval_service.rb +138 -0
  228. data/app/views/admin/shared/_tabs.html.erb +4 -0
  229. data/app/views/collavre/comments/_activity_log_details.html.erb +23 -0
  230. data/app/views/collavre/comments/_comment.html.erb +147 -0
  231. data/app/views/collavre/comments/_comments_popup.html.erb +46 -0
  232. data/app/views/collavre/comments/_list.html.erb +10 -0
  233. data/app/views/collavre/comments/_presence_avatars.html.erb +8 -0
  234. data/app/views/collavre/comments/_reaction_picker.html.erb +15 -0
  235. data/app/views/collavre/comments/_read_receipts.html.erb +19 -0
  236. data/app/views/collavre/creatives/_add_button.html.erb +20 -0
  237. data/app/views/collavre/creatives/_delete_button.html.erb +12 -0
  238. data/app/views/collavre/creatives/_github_integration_modal.html.erb +77 -0
  239. data/app/views/collavre/creatives/_import_upload_zone.html.erb +10 -0
  240. data/app/views/collavre/creatives/_inline_edit_form.html.erb +89 -0
  241. data/app/views/collavre/creatives/_mobile_actions_menu.html.erb +24 -0
  242. data/app/views/collavre/creatives/_notion_integration_modal.html.erb +90 -0
  243. data/app/views/collavre/creatives/_set_plan_modal.html.erb +25 -0
  244. data/app/views/collavre/creatives/_share_button.html.erb +55 -0
  245. data/app/views/collavre/creatives/edit.html.erb +4 -0
  246. data/app/views/collavre/creatives/index.html.erb +192 -0
  247. data/app/views/collavre/creatives/new.html.erb +4 -0
  248. data/app/views/collavre/creatives/show.html.erb +29 -0
  249. data/app/views/collavre/creatives/slide_view.html.erb +20 -0
  250. data/app/views/collavre/email_verification_mailer/verify.html.erb +4 -0
  251. data/app/views/collavre/email_verification_mailer/verify.text.erb +2 -0
  252. data/app/views/collavre/emails/index.html.erb +19 -0
  253. data/app/views/collavre/emails/show.html.erb +5 -0
  254. data/app/views/collavre/inbox_items/_item.html.erb +18 -0
  255. data/app/views/collavre/inbox_items/_items.html.erb +3 -0
  256. data/app/views/collavre/inbox_items/_list.html.erb +7 -0
  257. data/app/views/collavre/inbox_items/index.html.erb +1 -0
  258. data/app/views/collavre/inbox_mailer/daily_summary.html.erb +11 -0
  259. data/app/views/collavre/inbox_mailer/daily_summary.text.erb +8 -0
  260. data/app/views/collavre/invitation_mailer/invite.html.erb +5 -0
  261. data/app/views/collavre/invitation_mailer/invite.text.erb +3 -0
  262. data/app/views/collavre/invites/show.html.erb +8 -0
  263. data/app/views/collavre/passwords/edit.html.erb +12 -0
  264. data/app/views/collavre/passwords/new.html.erb +17 -0
  265. data/app/views/collavre/passwords_mailer/reset.html.erb +4 -0
  266. data/app/views/collavre/passwords_mailer/reset.text.erb +2 -0
  267. data/app/views/collavre/sessions/new.html.erb +27 -0
  268. data/app/views/collavre/sessions/providers/_google.html.erb +7 -0
  269. data/app/views/collavre/sessions/providers/_passkey.html.erb +3 -0
  270. data/app/views/collavre/shared/_link_creative_modal.html.erb +6 -0
  271. data/app/views/collavre/shared/_navigation.html.erb +37 -0
  272. data/app/views/collavre/shared/navigation/_help_button.html.erb +1 -0
  273. data/app/views/collavre/shared/navigation/_inbox_button.html.erb +5 -0
  274. data/app/views/collavre/shared/navigation/_mobile_inbox_button.html.erb +3 -0
  275. data/app/views/collavre/shared/navigation/_mobile_plans_button.html.erb +1 -0
  276. data/app/views/collavre/shared/navigation/_panels.html.erb +18 -0
  277. data/app/views/collavre/shared/navigation/_plans_button.html.erb +3 -0
  278. data/app/views/collavre/shared/navigation/_search_form.html.erb +6 -0
  279. data/app/views/collavre/user_themes/index.html.erb +75 -0
  280. data/app/views/collavre/users/_app_header.html.erb +5 -0
  281. data/app/views/collavre/users/_contact_management.html.erb +77 -0
  282. data/app/views/collavre/users/_id_pwd_fields.html.erb +27 -0
  283. data/app/views/collavre/users/edit_ai.html.erb +79 -0
  284. data/app/views/collavre/users/edit_password.html.erb +27 -0
  285. data/app/views/collavre/users/index.html.erb +80 -0
  286. data/app/views/collavre/users/new.html.erb +33 -0
  287. data/app/views/collavre/users/new_ai.html.erb +87 -0
  288. data/app/views/collavre/users/passkeys.html.erb +43 -0
  289. data/app/views/collavre/users/show.html.erb +143 -0
  290. data/app/views/inbox/badge_component/_count.html.erb +2 -0
  291. data/app/views/layouts/collavre/slide.html.erb +29 -0
  292. data/config/locales/comments.en.yml +114 -0
  293. data/config/locales/comments.ko.yml +110 -0
  294. data/config/locales/contacts.en.yml +16 -0
  295. data/config/locales/contacts.ko.yml +16 -0
  296. data/config/locales/creatives.en.yml +183 -0
  297. data/config/locales/creatives.ko.yml +164 -0
  298. data/config/locales/invites.en.yml +19 -0
  299. data/config/locales/invites.ko.yml +17 -0
  300. data/config/locales/notifications.en.yml +8 -0
  301. data/config/locales/notifications.ko.yml +8 -0
  302. data/config/locales/plans.en.yml +12 -0
  303. data/config/locales/plans.ko.yml +19 -0
  304. data/config/locales/themes.en.yml +29 -0
  305. data/config/locales/themes.ko.yml +27 -0
  306. data/config/locales/users.en.yml +151 -0
  307. data/config/locales/users.ko.yml +146 -0
  308. data/config/routes.rb +92 -0
  309. data/db/migrate/20241201000000_create_notion_integrations.rb +29 -0
  310. data/db/migrate/20250128110017_create_creatives.rb +11 -0
  311. data/db/migrate/20250128120122_create_users.rb +11 -0
  312. data/db/migrate/20250128120123_create_sessions.rb +11 -0
  313. data/db/migrate/20250128123633_create_subscribers.rb +10 -0
  314. data/db/migrate/20250312000000_create_notion_block_links.rb +16 -0
  315. data/db/migrate/20250312010000_allow_multiple_notion_blocks_per_creative.rb +5 -0
  316. data/db/migrate/20250522115048_remove_name_from_creatives.rb +5 -0
  317. data/db/migrate/20250522190651_add_parent_id_to_creatives.rb +5 -0
  318. data/db/migrate/20250523133100_rename_inventory_count_to_progress.rb +13 -0
  319. data/db/migrate/20250523133101_add_sequence_to_creatives.rb +5 -0
  320. data/db/migrate/20250525205100_add_user_id_to_creatives.rb +10 -0
  321. data/db/migrate/20250527014217_create_creative_shares.rb +12 -0
  322. data/db/migrate/20250528142349_add_origin_id_to_creatives.rb +5 -0
  323. data/db/migrate/20250530060200_create_tags.rb +9 -0
  324. data/db/migrate/20250531105150_add_value_to_tags.rb +5 -0
  325. data/db/migrate/20250531140142_create_labels.rb +12 -0
  326. data/db/migrate/20250531140145_change_tag_to_label_reference.rb +6 -0
  327. data/db/migrate/20250601000000_create_comments.rb +10 -0
  328. data/db/migrate/20250601061830_drop_plans_and_variations.rb +6 -0
  329. data/db/migrate/20250604122600_add_owner_to_labels.rb +5 -0
  330. data/db/migrate/20250606000000_rename_email_address_to_email_in_users.rb +7 -0
  331. data/db/migrate/20250606150329_create_creative_hierarchies.rb +16 -0
  332. data/db/migrate/20250610142000_create_creative_expanded_states.rb +11 -0
  333. data/db/migrate/20250611030138_create_invitations.rb +15 -0
  334. data/db/migrate/20250611105524_create_inbox_items.rb +14 -0
  335. data/db/migrate/20250612150000_update_permissions.rb +43 -0
  336. data/db/migrate/20250612232913_add_email_verified_at_to_users.rb +5 -0
  337. data/db/migrate/20250616065905_add_avatar_to_users.rb +5 -0
  338. data/db/migrate/20250617092111_add_display_level_to_users.rb +5 -0
  339. data/db/migrate/20250620004558_create_emails.rb +13 -0
  340. data/db/migrate/20250621000000_create_comment_read_pointers.rb +11 -0
  341. data/db/migrate/20250622000000_add_completion_mark_to_users.rb +5 -0
  342. data/db/migrate/20250623000000_add_theme_to_users.rb +5 -0
  343. data/db/migrate/20250624000000_add_name_to_users.rb +18 -0
  344. data/db/migrate/20250714190000_create_devices.rb +17 -0
  345. data/db/migrate/20250715120000_add_notifications_enabled_to_users.rb +5 -0
  346. data/db/migrate/20250823000000_add_no_access_permission.rb +25 -0
  347. data/db/migrate/20250826000000_add_calendar_id_to_users.rb +5 -0
  348. data/db/migrate/20250827000000_add_google_oauth_tokens_to_users.rb +8 -0
  349. data/db/migrate/20250827061238_add_timezone_to_users.rb +5 -0
  350. data/db/migrate/20250828000000_create_calendar_events.rb +16 -0
  351. data/db/migrate/20250828060000_add_creative_to_calendar_events.rb +5 -0
  352. data/db/migrate/20250830141052_add_locale_to_users.rb +5 -0
  353. data/db/migrate/20250830141101_add_message_key_to_inbox_items.rb +6 -0
  354. data/db/migrate/20250902025423_remove_description_and_featured_image_from_creatives.rb +6 -0
  355. data/db/migrate/20250910000000_add_private_to_comments.rb +5 -0
  356. data/db/migrate/20250910105640_migrate_write_permissions_to_admin.rb +17 -0
  357. data/db/migrate/20250911084338_backfill_creative_in_comment_inbox_items.rb +27 -0
  358. data/db/migrate/20250923002959_deduplicate_device_fcm_tokens.rb +25 -0
  359. data/db/migrate/20250924000000_add_system_admin_to_users.rb +6 -0
  360. data/db/migrate/20250925000000_create_github_integrations.rb +26 -0
  361. data/db/migrate/20250927000000_add_webhook_secret_to_github_repository_links.rb +29 -0
  362. data/db/migrate/20250928000000_add_action_and_approver_to_comments.rb +6 -0
  363. data/db/migrate/20250928010000_add_action_execution_tracking_to_comments.rb +6 -0
  364. data/db/migrate/20250928105957_add_github_gemini_prompt_to_creatives.rb +5 -0
  365. data/db/migrate/20250929000000_add_comment_and_creative_refs_to_inbox_items.rb +6 -0
  366. data/db/migrate/20251001000001_create_contacts.rb +71 -0
  367. data/db/migrate/20251002000000_add_shared_by_to_creative_shares.rb +14 -0
  368. data/db/migrate/20251124120902_add_ai_fields_to_users.rb +7 -0
  369. data/db/migrate/20251124122218_add_created_by_id_to_users.rb +5 -0
  370. data/db/migrate/20251124124521_add_llm_api_key_to_users.rb +5 -0
  371. data/db/migrate/20251124130000_add_searchable_to_users.rb +6 -0
  372. data/db/migrate/20251125072705_migrate_linked_creative_children_to_origin.rb +28 -0
  373. data/db/migrate/20251126040752_add_description_to_creatives.rb +75 -0
  374. data/db/migrate/20251127000000_move_comments_from_linked_creatives_to_origins.rb +18 -0
  375. data/db/migrate/20251201073823_add_tools_to_users.rb +5 -0
  376. data/db/migrate/20251202062715_create_mcp_tools.rb +16 -0
  377. data/db/migrate/20251204125754_create_tasks_and_task_actions.rb +23 -0
  378. data/db/migrate/20251204125756_add_routing_expression_to_users.rb +5 -0
  379. data/db/migrate/20251204161133_set_default_routing_expression_for_ai_agents.rb +13 -0
  380. data/db/migrate/20251211033025_nullify_self_referencing_origins.rb +9 -0
  381. data/db/migrate/20251211080040_create_topics_and_add_topic_to_comments.rb +15 -0
  382. data/db/migrate/20251215143500_create_activity_logs.rb +16 -0
  383. data/db/migrate/20251222125727_create_webauthn_credentials.rb +14 -0
  384. data/db/migrate/20251222125839_add_webauthn_id_to_users.rb +5 -0
  385. data/db/migrate/20251223022315_create_comment_reactions.rb +13 -0
  386. data/db/migrate/20251223072625_create_user_themes.rb +11 -0
  387. data/db/migrate/20251230074456_add_creative_id_to_labels.rb +5 -0
  388. data/db/migrate/20251230113607_refactor_labels.rb +38 -0
  389. data/db/migrate/20251231010012_backfill_tags_for_labels.rb +15 -0
  390. data/db/migrate/20251231013234_drop_subscribers.rb +9 -0
  391. data/db/migrate/20260106090544_allow_null_user_id_in_creative_shares.rb +6 -0
  392. data/db/migrate/20260106160643_create_system_settings.rb +11 -0
  393. data/db/migrate/20260116000000_create_creative_shares_cache.rb +15 -0
  394. data/db/migrate/20260116000001_populate_creative_shares_cache.rb +23 -0
  395. data/db/migrate/20260119022933_make_source_share_id_nullable_in_creative_shares_caches.rb +7 -0
  396. data/db/migrate/20260119023446_populate_owner_cache_entries.rb +25 -0
  397. data/db/migrate/20260119100000_add_account_lockout_to_users.rb +6 -0
  398. data/db/migrate/20260119110000_add_last_active_at_to_sessions.rb +6 -0
  399. data/db/migrate/20260120045354_encrypt_oauth_tokens.rb +60 -0
  400. data/db/migrate/20260120162259_remove_fk_from_creative_shares_caches.rb +7 -0
  401. data/db/migrate/20260120163856_remove_timestamps_from_creative_shares_caches.rb +6 -0
  402. data/lib/collavre/configuration.rb +14 -0
  403. data/lib/collavre/engine.rb +77 -0
  404. data/lib/collavre/user_extensions.rb +29 -0
  405. data/lib/collavre/version.rb +3 -0
  406. data/lib/collavre.rb +26 -0
  407. data/lib/generators/collavre/install/install_generator.rb +105 -0
  408. data/lib/generators/collavre/install/templates/build.cjs.tt +100 -0
  409. data/lib/tasks/collavre_assets.rake +15 -0
  410. metadata +591 -0
@@ -0,0 +1,280 @@
1
+ module Collavre
2
+ require "json"
3
+
4
+ module Github
5
+ class PullRequestAnalyzer
6
+ Result = Struct.new(:completed, :additional, :raw_response, :prompt, keyword_init: true)
7
+ CompletedTask = Struct.new(:creative_id, :progress, :note, :path, keyword_init: true)
8
+ SuggestedTask = Struct.new(:parent_id, :description, :progress, :note, :path, keyword_init: true)
9
+
10
+ DIFF_MAX_LENGTH = 10_000
11
+
12
+ def initialize(payload:, creative:, paths:, commit_messages: [], diff: nil, client: default_client, logger: Rails.logger)
13
+ @payload = payload
14
+ @creative = creative
15
+ @paths = normalize_paths(paths)
16
+ @commit_messages = Array(commit_messages)
17
+ @diff = diff
18
+ @client = client
19
+ @logger = logger
20
+ @prompt_text = nil
21
+ end
22
+
23
+ def call
24
+ response_text = collect_response
25
+ return unless response_text.present?
26
+
27
+ parsed = parse_response(response_text)
28
+ Result.new(
29
+ completed: parsed[:completed],
30
+ additional: parsed[:additional],
31
+ raw_response: response_text,
32
+ prompt: prompt_text
33
+ )
34
+ rescue StandardError => e
35
+ logger.error("Gemini analysis failed: #{e.class} #{e.message}")
36
+ nil
37
+ end
38
+
39
+ private
40
+
41
+ attr_reader :payload, :creative, :paths, :commit_messages, :diff, :client, :logger, :prompt_text
42
+
43
+ def collect_response
44
+ messages = build_messages
45
+ buffer = +""
46
+ client.chat(messages) do |delta|
47
+ buffer << delta.to_s
48
+ end
49
+ buffer
50
+ rescue StandardError => e
51
+ logger.error("Gemini chat failed: #{e.class} #{e.message}")
52
+ nil
53
+ end
54
+
55
+ def default_client
56
+ AiClient.new(
57
+ vendor: "google",
58
+ model: "gemini-2.5-flash",
59
+ system_prompt: AiClient::SYSTEM_INSTRUCTIONS
60
+ )
61
+ end
62
+
63
+ def build_messages
64
+ pr = payload["pull_request"]
65
+
66
+ # Use shared TreeFormatter logic
67
+ tree_lines = Creatives::TreeFormatter.new.format(creative)
68
+
69
+ pr_body = pr["body"].to_s
70
+ commit_lines = formatted_commit_messages
71
+ diff_text = formatted_diff
72
+ language_instructions = preferred_language_instructions
73
+
74
+ prompt_template = creative.github_gemini_prompt_template
75
+ prompt = render_prompt_template(
76
+ prompt_template,
77
+ pr_title: pr["title"].to_s,
78
+ pr_body: pr_body,
79
+ commit_messages: commit_lines,
80
+ diff: diff_text,
81
+ creative_tree: tree_lines,
82
+ language_instructions: language_instructions
83
+ )
84
+
85
+ @prompt_text = prompt
86
+ [ { role: "user", parts: [ { text: prompt } ] } ]
87
+ end
88
+
89
+ def normalize_paths(paths)
90
+ Array(paths).map do |entry|
91
+ case entry
92
+ when Hash
93
+ path_value = entry[:path]
94
+ path_value = entry["path"] if path_value.nil?
95
+
96
+ leaf_value =
97
+ if entry.key?(:leaf)
98
+ entry[:leaf]
99
+ elsif entry.key?("leaf")
100
+ entry["leaf"]
101
+ end
102
+
103
+ {
104
+ path: path_value.to_s,
105
+ leaf: !!leaf_value
106
+ }
107
+ else
108
+ { path: entry.to_s, leaf: false }
109
+ end
110
+ end
111
+ end
112
+
113
+ def parse_response(text)
114
+ json_fragment = extract_json(text)
115
+ data = JSON.parse(json_fragment)
116
+ {
117
+ completed: sanitize_completed(data["completed"]),
118
+ additional: sanitize_additional(data["additional"])
119
+ }
120
+ rescue JSON::ParserError => e
121
+ logger.warn("Failed to parse Gemini response as JSON: #{e.message}")
122
+ { completed: [], additional: [] }
123
+ end
124
+
125
+ def extract_json(text)
126
+ start_index = text.index("{")
127
+ end_index = text.rindex("}")
128
+ return text unless start_index && end_index && end_index >= start_index
129
+
130
+ text[start_index..end_index]
131
+ end
132
+
133
+ def formatted_commit_messages
134
+ return "No commit messages available." if commit_messages.blank?
135
+
136
+ commit_messages.map.with_index(1) do |message, index|
137
+ "#{index}. #{message.to_s.strip}"
138
+ end.join("\n")
139
+ end
140
+
141
+ def formatted_diff
142
+ return "(No diff available)" if diff.blank?
143
+
144
+ diff_text = diff.to_s.strip
145
+ return "(No diff available)" if diff_text.empty?
146
+
147
+ return diff_text if diff_text.length <= DIFF_MAX_LENGTH
148
+
149
+ truncated = diff_text.slice(0, DIFF_MAX_LENGTH)
150
+ "#{truncated}\n...\n[Diff truncated to #{DIFF_MAX_LENGTH} characters]"
151
+ end
152
+
153
+ def preferred_language_instructions
154
+ language = preferred_response_language
155
+ "Preferred response language: #{language[:label]} (#{language[:code]}). Write all natural-language output, including new creative descriptions, in #{language[:label]}."
156
+ end
157
+
158
+ def preferred_response_language
159
+ locale = creative.user&.locale.presence
160
+ locale ||= I18n.default_locale.to_s if defined?(I18n)
161
+ locale ||= "en"
162
+
163
+ label = if defined?(I18n)
164
+ I18n.t("collavre.users.locales.#{locale}", default: locale)
165
+ else
166
+ locale
167
+ end
168
+
169
+ { code: locale, label: label }
170
+ end
171
+
172
+ def render_prompt_template(template, variables)
173
+ template.to_s.gsub(/\#\{([^}]+)\}/) do
174
+ key = Regexp.last_match(1).strip.to_sym
175
+ value = variables.fetch(key, "")
176
+ value.to_s
177
+ end
178
+ end
179
+
180
+ def sanitize_completed(items)
181
+ Array(items).filter_map do |item|
182
+ case item
183
+ when Hash
184
+ creative_id = extract_creative_id(item["creative_id"] || item["id"])
185
+ next unless creative_id
186
+
187
+ CompletedTask.new(
188
+ creative_id: creative_id,
189
+ progress: normalize_progress(item["progress"], default: 1.0),
190
+ note: string_presence(item["note"] || item["summary"]),
191
+ path: string_presence(item["path"] || item["description"])
192
+ )
193
+ when Integer
194
+ CompletedTask.new(creative_id: item, progress: 1.0)
195
+ when String
196
+ creative_id = extract_id_from_string(item)
197
+ next unless creative_id
198
+
199
+ CompletedTask.new(
200
+ creative_id: creative_id,
201
+ progress: 1.0,
202
+ path: string_presence(item)
203
+ )
204
+ else
205
+ nil
206
+ end
207
+ end
208
+ end
209
+
210
+ def sanitize_additional(items)
211
+ Array(items).filter_map do |item|
212
+ case item
213
+ when Hash
214
+ parent_id = extract_creative_id(item["parent_id"] || item["parent"])
215
+ description = string_presence(item["description"] || item["title"])
216
+ next unless parent_id && description
217
+
218
+ SuggestedTask.new(
219
+ parent_id: parent_id,
220
+ description: description,
221
+ progress: normalize_progress(item["progress"], default: nil),
222
+ note: string_presence(item["note"] || item["summary"]),
223
+ path: string_presence(item["path"])
224
+ )
225
+ when String
226
+ parent_id = extract_id_from_string(item)
227
+ next unless parent_id
228
+
229
+ description = item.sub(/^.*\]\s*/, "").strip
230
+ description = item if description.blank?
231
+
232
+ SuggestedTask.new(parent_id: parent_id, description: description, progress: nil)
233
+ else
234
+ nil
235
+ end
236
+ end
237
+ end
238
+
239
+ def extract_creative_id(value)
240
+ case value
241
+ when Integer
242
+ value.positive? ? value : nil
243
+ when Float
244
+ int_value = value.to_i
245
+ int_value.positive? ? int_value : nil
246
+ when String
247
+ match = value.match(/\d+/)
248
+ match ? match[0].to_i : nil
249
+ else
250
+ nil
251
+ end
252
+ end
253
+
254
+ def normalize_progress(value, default: nil)
255
+ return default if value.nil?
256
+
257
+ float_value = Float(value) rescue nil
258
+ return default unless float_value
259
+
260
+ [ [ float_value, 0.0 ].max, 1.0 ].min
261
+ end
262
+
263
+ def string_presence(value)
264
+ return if value.nil?
265
+
266
+ str = value.to_s.strip
267
+ str.presence
268
+ end
269
+
270
+ def extract_id_from_string(value)
271
+ return unless value.is_a?(String)
272
+
273
+ matches = value.scan(/\[(\d+)\]/).flatten
274
+ return if matches.blank?
275
+
276
+ matches.last.to_i
277
+ end
278
+ end
279
+ end
280
+ end
@@ -0,0 +1,181 @@
1
+ module Collavre
2
+ require "json"
3
+
4
+ module Github
5
+ class PullRequestProcessor
6
+ HANDLED_ACTIONS = %w[closed].freeze
7
+
8
+ def initialize(payload:, logger: Rails.logger)
9
+ @payload = payload
10
+ @logger = logger
11
+ end
12
+
13
+ def call
14
+ action = payload["action"]
15
+ return unless HANDLED_ACTIONS.include?(action)
16
+
17
+ pr = payload["pull_request"]
18
+ return unless pr
19
+ return unless pr["merged"]
20
+
21
+ repo_full_name = payload.dig("repository", "full_name")
22
+ return unless repo_full_name
23
+
24
+ links = GithubRepositoryLink.includes(:creative).where(repository_full_name: repo_full_name)
25
+ return if links.blank?
26
+
27
+ links.each do |link|
28
+ process_link(link)
29
+ end
30
+ rescue StandardError => e
31
+ logger.error("GitHub PR processing failed: #{e.class} #{e.message}")
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :payload, :logger
37
+
38
+ def process_link(link)
39
+ creative = link.creative.effective_origin
40
+ path_exporter = Creatives::PathExporter.new(creative, use_effective_origin: false)
41
+ tree_entries = path_exporter.full_paths_with_ids_and_progress_with_leaf
42
+ return if tree_entries.blank?
43
+
44
+ repo_full_name = payload.dig("repository", "full_name")
45
+ pr = payload["pull_request"]
46
+ client = Github::Client.new(link.github_account)
47
+ commit_messages = client.pull_request_commit_messages(repo_full_name, pr["number"])
48
+ diff = client.pull_request_diff(repo_full_name, pr["number"])
49
+
50
+ analyzer = Github::PullRequestAnalyzer.new(
51
+ payload: payload,
52
+ creative: creative,
53
+ paths: tree_entries,
54
+ commit_messages: commit_messages,
55
+ diff: diff
56
+ )
57
+ result = analyzer.call
58
+ return unless result
59
+
60
+ create_comment(creative, link, path_exporter, result)
61
+ end
62
+
63
+ def create_comment(creative, link, path_exporter, result)
64
+ pr = payload["pull_request"]
65
+ title = pr["title"]
66
+ number = pr["number"]
67
+ url = pr["html_url"]
68
+
69
+ lines = []
70
+ lines << "### Github PR 분석"
71
+ lines << "- PR: [##{number} #{title}](#{url})"
72
+ lines << ""
73
+ lines << "#### 완료된 Creative"
74
+ if result.completed.any?
75
+ result.completed.each do |task|
76
+ lines << "- #{format_completed_task(task, path_exporter)}"
77
+ end
78
+ else
79
+ lines << "- 없음"
80
+ end
81
+ lines << ""
82
+ lines << "#### 추가로 필요한 Creative"
83
+ if result.additional.any?
84
+ result.additional.each do |suggestion|
85
+ lines << "- #{format_suggestion(suggestion, path_exporter)}"
86
+ end
87
+ else
88
+ lines << "- 없음"
89
+ end
90
+
91
+ actions = build_actions(result)
92
+ if actions.any?
93
+ lines << ""
94
+ lines << "#### 승인 시 자동 적용"
95
+ lines << "- 이 댓글을 승인하면 완료된 Creative의 진행률이 업데이트되고 제안된 Creative가 생성됩니다."
96
+ end
97
+
98
+ attributes = { user: nil, content: lines.join("\n") }
99
+ if actions.any?
100
+ approver = link.github_account.user
101
+ if approver
102
+ attributes[:action] = JSON.pretty_generate({ actions: actions })
103
+ attributes[:approver] = approver
104
+ else
105
+ logger.warn("Skipping action assignment for PR comment because approver is missing")
106
+ end
107
+ end
108
+
109
+ creative.comments.create!(attributes)
110
+ end
111
+
112
+ def build_actions(result)
113
+ actions = []
114
+
115
+ result.completed.each do |task|
116
+ next unless task.creative_id
117
+
118
+ actions << {
119
+ "action" => "update_creative",
120
+ "creative_id" => task.creative_id,
121
+ "attributes" => {
122
+ "progress" => (task.progress || 1.0).to_f
123
+ }
124
+ }
125
+ end
126
+
127
+ result.additional.each do |suggestion|
128
+ next unless suggestion.parent_id && suggestion.description.present?
129
+
130
+ attributes = { "description" => suggestion.description }
131
+ attributes["progress"] = suggestion.progress.to_f if suggestion.progress
132
+
133
+ actions << {
134
+ "action" => "create_creative",
135
+ "parent_id" => suggestion.parent_id,
136
+ "attributes" => attributes
137
+ }
138
+ end
139
+
140
+ actions
141
+ end
142
+
143
+ def format_completed_task(task, path_exporter)
144
+ label = task.path.presence || path_exporter.path_for(task.creative_id) || "Creative ##{task.creative_id}"
145
+ parts = []
146
+ parts << "#{creative_link(task.creative_id)} #{label}"
147
+ if task.progress && task.progress < 1.0
148
+ percentage = (task.progress * 100).round
149
+ parts << "(progress #{percentage}%)"
150
+ end
151
+ parts << task.note if task.note.present?
152
+ parts.join(" ")
153
+ end
154
+
155
+ def format_suggestion(suggestion, path_exporter)
156
+ parent_label = path_exporter.path_for(suggestion.parent_id) || "Creative ##{suggestion.parent_id}"
157
+ parts = []
158
+ parts << "#{creative_link(suggestion.parent_id)} #{parent_label}"
159
+ parts << "→ #{suggestion.description}"
160
+ if suggestion.progress
161
+ percentage = (suggestion.progress * 100).round
162
+ parts << "(initial progress #{percentage}%)"
163
+ end
164
+ parts << "- #{suggestion.note}" if suggestion.note.present?
165
+ parts.join(" ")
166
+ end
167
+
168
+ def creative_link(id)
169
+ path = url_helpers.creative_path(id)
170
+ "[#%<id>d](%<path>s)" % { id: id, path: path }
171
+ rescue StandardError => e
172
+ logger.warn("Failed to build creative link for ##{id}: #{e.message}")
173
+ "##{id}"
174
+ end
175
+
176
+ def url_helpers
177
+ @url_helpers ||= Collavre::Engine.routes.url_helpers
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,130 @@
1
+ module Collavre
2
+ module Github
3
+ class WebhookProvisioner
4
+ EVENTS = %w[pull_request].freeze
5
+ CONTENT_TYPE = "json".freeze
6
+
7
+ def self.ensure_for_links(account:, links:, webhook_url:)
8
+ new(account: account, webhook_url: webhook_url).ensure_for_links(Array(links))
9
+ end
10
+
11
+ def self.remove_for_repositories(account:, repositories:, webhook_url:)
12
+ new(account: account, webhook_url: webhook_url).remove_for_repositories(Array(repositories))
13
+ end
14
+
15
+ def initialize(account:, webhook_url:, client: Github::Client.new(account))
16
+ @client = client
17
+ @webhook_url = webhook_url
18
+ end
19
+
20
+ def ensure_for_links(links)
21
+ links.each do |link|
22
+ ensure_webhook(link)
23
+ end
24
+ end
25
+
26
+ def remove_for_repositories(repositories)
27
+ repositories.each do |repository_full_name|
28
+ next if GithubRepositoryLink.where(repository_full_name: repository_full_name).exists?
29
+
30
+ remove_webhook(repository_full_name)
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :client, :webhook_url
37
+
38
+ def ensure_webhook(link)
39
+ repository_full_name = link.repository_full_name
40
+ primary_link = primary_link_for(repository_full_name)
41
+ hook = find_existing_hook(repository_full_name)
42
+
43
+ if hook
44
+ if primary_link && primary_link != link
45
+ align_link_secret(link, primary_link.webhook_secret)
46
+ else
47
+ update_webhook(repository_full_name, hook.id, link.webhook_secret)
48
+ end
49
+ else
50
+ secret = link.webhook_secret
51
+
52
+ if primary_link && primary_link != link
53
+ secret = primary_link.webhook_secret
54
+ align_link_secret(link, secret)
55
+ end
56
+
57
+ create_webhook(repository_full_name, secret)
58
+ end
59
+ rescue Octokit::Error => e
60
+ Rails.logger.warn(
61
+ "GitHub webhook provisioning failed for #{repository_full_name}: #{e.message}"
62
+ )
63
+ end
64
+
65
+ def remove_webhook(repository_full_name)
66
+ hook = find_existing_hook(repository_full_name)
67
+ return unless hook
68
+
69
+ client.delete_repository_webhook(repository_full_name, hook.id)
70
+ rescue Octokit::Error => e
71
+ Rails.logger.warn(
72
+ "GitHub webhook removal failed for #{repository_full_name}: #{e.message}"
73
+ )
74
+ end
75
+
76
+ def find_existing_hook(repository_full_name)
77
+ client.repository_hooks(repository_full_name).find do |hook|
78
+ config = normalize_config(hook.config)
79
+ config["url"] == webhook_url
80
+ end
81
+ end
82
+
83
+ def create_webhook(repository_full_name, secret)
84
+ client.create_repository_webhook(
85
+ repository_full_name,
86
+ url: webhook_url,
87
+ secret: secret,
88
+ events: EVENTS,
89
+ content_type: CONTENT_TYPE
90
+ )
91
+ end
92
+
93
+ def update_webhook(repository_full_name, hook_id, secret)
94
+ client.update_repository_webhook(
95
+ repository_full_name,
96
+ hook_id,
97
+ url: webhook_url,
98
+ secret: secret,
99
+ events: EVENTS,
100
+ content_type: CONTENT_TYPE
101
+ )
102
+ end
103
+
104
+ def primary_link_for(repository_full_name)
105
+ GithubRepositoryLink
106
+ .where(repository_full_name: repository_full_name)
107
+ .order(:id)
108
+ .first
109
+ end
110
+
111
+ def align_link_secret(link, secret)
112
+ return if secret.blank? || link.webhook_secret == secret
113
+
114
+ link.update!(webhook_secret: secret)
115
+ end
116
+
117
+ def normalize_config(config)
118
+ hash =
119
+ case config
120
+ when Hash
121
+ config
122
+ else
123
+ config.respond_to?(:to_h) ? config.to_h : {}
124
+ end
125
+
126
+ hash.with_indifferent_access
127
+ end
128
+ end
129
+ end
130
+ end