fuik 0.11.0 → 0.12.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.
- checksums.yaml +4 -4
- data/README.md +7 -1
- data/app/assets/images/fuik/icons/46elks.jpg +0 -0
- data/app/assets/images/fuik/icons/lettermint.jpg +0 -0
- data/app/assets/images/fuik/icons/smtp2go.jpg +0 -0
- data/app/assets/javascript/attractivejs-0.12.0.min.js +1 -0
- data/app/assets/stylesheets/fuik/application.css +59 -0
- data/app/controllers/fuik/events_controller.rb +1 -1
- data/app/controllers/fuik/webhooks_controller.rb +20 -3
- data/app/jobs/fuik/webhook_processing_job.rb +7 -0
- data/app/models/fuik/webhook_event/filterable.rb +26 -0
- data/app/models/fuik/webhook_event.rb +2 -0
- data/app/views/fuik/events/_filters.html.erb +29 -0
- data/app/views/fuik/events/index.html.erb +2 -0
- data/app/views/layouts/fuik/application.html.erb +1 -1
- data/lib/fuik/version.rb +1 -1
- data/lib/generators/fuik/provider/templates/lettermint/base.rb.tt +32 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_failed.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_hard_bounced.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_soft_bounced.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/lettermint/message_suppressed.rb.tt +15 -0
- data/lib/generators/fuik/provider/templates/smtp2go/base.rb.tt +4 -0
- data/lib/generators/fuik/provider/templates/smtp2go/bounce.rb.tt +16 -0
- data/lib/generators/fuik/provider/templates/smtp2go/reject.rb.tt +14 -0
- data/lib/generators/fuik/provider/templates/smtp2go/spam.rb.tt +13 -0
- data/lib/generators/fuik/provider/templates/smtp2go/unsubscribe.rb.tt +13 -0
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4a653582dac2c822bb4496a89d0dbd12cd9b96a082bdeed11c209793df27de21
|
|
4
|
+
data.tar.gz: 44070a1620d3d867cc5a400e7f61bbd58bebec6e713407d6da6a65d8d2d2037b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c870f47e09396993506e96851c491d0e715ddf87d964554aba212e83927a2e9370c75b601cce24f8b4a0653b1b8cfe16f3825fb916048fa9828c05e95ab8d49a
|
|
7
|
+
data.tar.gz: 9ce68f8cb2d9aa7af1aa3cfb6a394a1a26cc165ed6dc58320fbfc0b266d219e839d0a29a352164fadce614ff75579dcb8041077d553cdbd45cfe6424d709125e
|
data/README.md
CHANGED
|
@@ -6,7 +6,6 @@ Fuik (Dutch for fish trap) is a Rails engine that catches and stores webhooks fr
|
|
|
6
6
|
|
|
7
7
|
<img alt="Fuik admin interface" src="https://raw.githubusercontent.com/Rails-Designer/fuik/HEAD/.github/docs/webhooks-index.jpg" style="max-width: 100%;">
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
**Sponsored By [Rails Designer](https://railsdesigner.com/)**
|
|
11
10
|
|
|
12
11
|
<a href="https://railsdesigner.com/" target="_blank">
|
|
@@ -60,6 +59,13 @@ Visit `/webhooks` to see all received webhooks. Click any event to view all the
|
|
|
60
59
|
|
|
61
60
|
⚠️ The `/webhooks` path is by default not protected. Easiest is to set `Fuik::Engine.config.events_controller_parent` to a controller that requires authentication.
|
|
62
61
|
|
|
62
|
+
### Dashboard features
|
|
63
|
+
|
|
64
|
+
* **Copy payload as JSON**: click a button, payload is in your clipboard
|
|
65
|
+
* **Download payload as JSON file**: keep it for testing, debugging or throw it at your LLM agent, bot or colleague
|
|
66
|
+
* **Add `.json` to any URL**: get the raw payload without the UI
|
|
67
|
+
* **Click any key to get the Ruby accessor path**: click `product_id` (as seen in the screenshot above) and get `payload["line_items"][0]["product_id"]` (say what? 🤯)
|
|
68
|
+
|
|
63
69
|
|
|
64
70
|
### Add business logic
|
|
65
71
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).Attractive=e()}(this,(function(){"use strict";class t{static enabled=!1;static prefix="🧲 ";static log(...t){this.enabled&&console.log(this.prefix,...t)}static warn(...t){this.enabled&&console.warn(this.prefix,...t)}static error(...t){this.enabled&&console.error(this.prefix,...t)}static throw(t){throw new Error(`${this.prefix}${t}`)}}class e{#t;constructor(e){t.log("Events with actions:",Object.keys(e)),this.#t=e}process(t,{on:e,using:s}){e&&this.#e(e.dataset.action).some((r=>!1===this.#s(r,{for:t,on:e,using:s})))}#e(t){return t.split(" ").filter((t=>t))}#s(e,{for:s,on:r,using:i}){if(t.log("Process action for",s.type,"on",r,"…"),e.includes(":")){const[t,r]=e.split(":");if(s.type!==r)return;e=t}else if(e.includes("->")){const[t,r]=e.split("->");if((t.includes("@")?t.split("@")[1]:t)!==s.type)return;e=r}else if(s.type!==i)return;return t.log("…","processed action for",s.type,"on",r),this.#r(e,{on:r,for:s})}#r(e,{on:s,for:r}){t.log("Execute action",e,"on",s,"…");const i=e.split("#"),[n,a,o]=i,c=this.#t[n]?n:a??e;if("function"!=typeof this.#t[c])return;const l=this.#t[n]?i.slice(1).join("#"):o??null,u=this.#t[c](s,{value:l,target:s.dataset.target,targets:s.dataset.targets});return!1===u&&r&&r.preventDefault(),t.log("…","executed action",e,"on",s),u}}var s=new class{identify({by:t}){return t.split(" ").filter((t=>t.includes("->"))).map((t=>t.split("->")[0]))}getDefault({from:t}){const e=t.tagName.toLowerCase(),s="input"===e,r=s?t.type||"text":null;return s?this.#i.input[r]||this.#i.input.default:this.#i[e]||this.#i.default}#i={a:"click",button:"click",input:{checkbox:"change",radio:"change",submit:"click",button:"click",reset:"click",default:"input"},select:"change",textarea:"input",form:"submit",default:"click"}};class r{#n;#a;#o;constructor(t,e=null){this.#n=t,this.#a=e}start(t){if(window.MutationObserver)return this.#o=new MutationObserver((e=>{const s=new Set,r=new Set;e.forEach((e=>this.#c(e,{for:t,elements:{added:s,removed:r}}))),s.forEach((t=>this.#n(t))),this.#a&&r.forEach((t=>this.#a(t)))})),this.#o.observe(document.documentElement,{childList:!0,subtree:!0}),this}stop(){return this.#o&&this.#o.disconnect(),this}#c(t,{for:e,elements:{added:s,removed:r}}){"childList"===t.type&&(t.addedNodes.forEach((t=>{this.#l(t,{for:e,and:s})})),t.removedNodes.forEach((t=>{this.#l(t,{for:e,and:r})})))}#l(t,{for:e,and:s}){t.nodeType===Node.ELEMENT_NODE&&(t.matches&&t.matches(e)&&s.add(t),t.querySelectorAll&&t.querySelectorAll(e).forEach((t=>s.add(t))))}}var i=new class{#u={mounted:(t,e)=>{e()},now:(t,e)=>{e()},whenVisible:(t,e)=>{const s=new IntersectionObserver((t=>{t.forEach((t=>{t.isIntersecting&&(e(),s.disconnect())}))}));s.observe(t)}};setup({for:t,on:e,trigger:s}){const r=this.#u[t];return!!r&&(r(e,s),!0)}};class n{constructor(t,e={}){if(!t)throw new Error("Current element is required");this.currentElement=t,this.target=e.target,this.targetsSelector=e.targets,this.options=e}get targets(){if(this.targetsSelector)return Array.from(document.querySelectorAll(this.targetsSelector));if(this.target){const t=document.getElementById(this.target);return t?[t]:[]}return[this.currentElement]}cycledValue(t,e){const s=Array.isArray(e)?e:e.split(",").map((t=>t.trim())),r=s.indexOf(t);return s[(r+1)%s.length]}}class a extends n{constructor(t,e={}){super(t,e);const[s,r]=e.value.split("=");this.attribute=s,this.value=r}toggle(){this.attribute&&this.targets.forEach((t=>t.hasAttribute(this.attribute)?t.removeAttribute(this.attribute):t.setAttribute(this.attribute,this.value||"")))}cycle(){this.value&&this.targets.forEach((t=>this.#h(t)))}add(){this.attribute&&this.targets.forEach((t=>t.setAttribute(this.attribute,this.value||"")))}remove(){this.attribute&&this.targets.forEach((t=>t.removeAttribute(this.attribute)))}#h(t){const e=this.cycledValue(t.getAttribute(this.attribute),this.value);t.setAttribute(this.attribute,e)}}const o=t=>(e,s={})=>new a(e,s)[t]();var c={toggleAttribute:o("toggle"),cycleAttribute:o("cycle"),addAttribute:o("add"),removeAttribute:o("remove")};const l=t=>t?.split(",").map((t=>t.trim())).filter(Boolean)??[];class u extends n{constructor(t,e={}){super(t,e),this.value=l(e.value)}toggle(){this.value&&this.targets.forEach((t=>this.#d({forEach:t})))}cycle(){this.value&&0!==this.value.length&&this.targets.forEach((t=>this.#f(t)))}add(){this.value&&this.targets.forEach((t=>t.classList.add(...this.value)))}remove(){this.value&&this.targets.forEach((t=>t.classList.remove(...this.value)))}#d({forEach:t}){this.value.forEach((e=>t.classList.toggle(e)))}#f(t){const e=this.value.find((e=>t.classList.contains(e)))||"",s=this.cycledValue(e,this.value);t.classList.remove(...this.value),t.classList.add(s)}}const h=t=>(e,s={})=>new u(e,s)[t]();var d={toggleClass:h("toggle"),cycleClass:h("cycle"),addClass:h("add"),removeClass:h("remove")};let f;const g=(t,e)=>{clearTimeout(f),f=setTimeout(t,e)};class v extends n{constructor(t,e={}){super(t,e),this.value=e.value}async copy(){const t=this.value||(this.targets[0]?.value??this.targets[0]?.textContent);if(void 0!==t)try{await navigator.clipboard.writeText(t),this.#g(!0)}catch(t){this.#g(!1)}}#g(t){const e=parseInt(this.currentElement.dataset.copyDelay);this.targets.forEach((e=>e.setAttribute(this.#v,t))),e&&g((()=>this.targets.forEach((t=>t.removeAttribute(this.#v)))),e)}get#v(){return"data-copy-success"}}var m,p={copy:(m="copy",(t,e={})=>new v(t,e)[m]())};class b extends n{confirm(){const t=this.currentElement.dataset.confirmMessage||"Are you sure?",e=window.confirm(t);return this.#g(e),e}#g(t){this.currentElement.setAttribute("data-confirm-success",t);const e=this.currentElement.dataset.confirmDuration;e&&g((()=>this.currentElement.removeAttribute("data-confirm-success")),parseInt(e))}}var E={confirm:(t=>(e,s={})=>new b(e,s)[t]())("confirm")};class w extends n{constructor(t,e={}){super(t,e);const[s,r]=e.value.split("=");this.attribute=s,this.value=r}toggle(){this.attribute&&this.targets.forEach((t=>{this.attribute in t.dataset?delete t.dataset[this.attribute]:t.dataset[this.attribute]=this.value||""}))}cycle(){this.value&&this.targets.forEach((t=>this.#m(t)))}add(){this.attribute&&this.targets.forEach((t=>{t.dataset[this.attribute]=this.value||""}))}remove(){this.attribute&&this.targets.forEach((t=>delete t.dataset[this.attribute]))}#m(t){const e=this.cycledValue(t.dataset[this.attribute],this.value);t.dataset[this.attribute]=e}}const y=t=>(e,s={})=>new w(e,s)[t]();var A={toggleDataAttribute:y("toggle"),cycleDataAttribute:y("cycle"),addDataAttribute:y("add"),removeDataAttribute:y("remove")};class L extends n{open(){this.targets.forEach((t=>t instanceof HTMLDialogElement&&t.show()))}openModal(){this.targets.forEach((t=>t instanceof HTMLDialogElement&&t.showModal()))}close(){this.targets.forEach((t=>t instanceof HTMLDialogElement&&t.close()))}}const T=t=>(e,s={})=>{new L(e,s)[t]()};var x={open:T("open"),openModal:T("openModal"),close:T("close")};class M extends n{add(){const t=this.targets[0];if(!t)return;const e=this.currentElement.dataset.addSource;if(!e)return;const s=document.querySelector(e);if(!s)return;const r=this.#p(s),i=this.currentElement.dataset.addAt||"beforeend";t.insertAdjacentElement(i,r)}remove(){const t=parseInt(this.currentElement.dataset.removeDelay),e=()=>{this.targets.forEach((t=>t.remove()))};t?setTimeout(e,t):e()}#p(t){return"TEMPLATE"===t.tagName?t.content.cloneNode(!0).firstElementChild:t.cloneNode(!0)}}const S=t=>(e,s={})=>new M(e,s)[t]();var k={add:S("add"),remove:S("remove")};class N extends n{requestSubmit(){const t=parseInt(this.currentElement.dataset.submitDelay)||0,e=()=>this.targets.forEach((t=>t instanceof HTMLFormElement&&t.requestSubmit()));t?g(e,t):e()}reset(){this.targets.forEach((t=>t instanceof HTMLFormElement&&t.reset()))}}const F=t=>(e,s={})=>{new N(e,s)[t]()};var D={submit:F("requestSubmit"),reset:F("reset")};class q extends n{constructor(t,e={}){super(t,e),this.classNames=l(e.value),this.justOnce="intersect-once"===e.actionName}start(){if(0===this.classNames.length)return;const t=new IntersectionObserver((e=>{e.forEach((e=>{e.isIntersecting?this.#b(t,e.target):this.#E()}))}));t.observe(this.currentElement)}#b(t,e){this.targets.forEach((t=>t.classList.add(...this.classNames))),this.justOnce&&t.unobserve(e)}#E(){this.justOnce||this.targets.forEach((t=>t.classList.remove(...this.classNames)))}}const C=t=>(e,s={})=>new q(e,{...s,actionName:t}).start();var O={"intersect-once":C("intersect-once"),"intersect-toggle":C("intersect-toggle")};class I extends n{reload(){this.targets.forEach((t=>{this.#w(t)?t.reload():window.location.reload()}))}#w(t){return"TURBO-FRAME"===t.tagName&&"function"==typeof t.reload}}const H=t=>(e,s={})=>new I(e,s)[t]();var V={reload:H("reload"),refresh:H("reload")};class j{static get(){const t=document.querySelector('meta[name="csrf-token"]')?.content;return t||null}}class $ extends n{constructor(t,e={}){super(t,e),this.value=e.value}async get(){if(this.value){this.#y({with:this.value})&&console.warn(`Cross-origin request to: ${this.value}. Missing the correct CORS headers.`),this.#g("busy");try{const t=await fetch(this.value,{method:"GET"});if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);const e=await t.text();return this.targets.forEach((t=>{t.innerHTML=e})),this.#g("success"),t}catch(t){throw console.error("GET request failed:",t),this.#g("error"),t}}else console.warn("No URL provided in the action value")}post(){return this.#A("POST")}patch(){return this.#A("PATCH")}put(){return this.#A("PUT")}#y({with:t}){try{return new URL(t,window.location.href).origin!==window.location.origin}catch{return console.error("Invalid URL:",t),!1}}#g(t){const e=this.currentElement.dataset.requestDuration;this.targets.forEach((e=>{"busy"===t?(e.setAttribute("data-request-busy","true"),e.removeAttribute("data-request-success")):(e.removeAttribute("data-request-busy"),e.setAttribute("data-request-success","success"===t))})),e&&"busy"!==t&&g((()=>{this.targets.forEach((t=>t.removeAttribute("data-request-success")))}),parseInt(e))}#A(t){if(this.value)return this.#g("busy"),fetch(this.value,{method:t,headers:{"Content-Type":"application/json","X-CSRF-Token":j.get()},body:JSON.stringify(this.#L)}).then((t=>{if(!t.ok)throw new Error(`HTTP error! status: ${t.status}`);return this.#g("success"),t})).catch((e=>{throw console.error(`${t} request failed:`,e),this.#g("error"),e}));console.warn("No URL provided in the action value")}get#L(){const t={},e=this.targets[0];if(e instanceof HTMLFormElement){const s=new FormData(e);for(const[e,r]of s.entries())t[e]=r;return t}if(this.#T){const e=this.currentElement.name,s=this.currentElement.value;t[e]=s}return t}get#T(){return this.currentElement instanceof HTMLInputElement||this.currentElement instanceof HTMLSelectElement||this.currentElement instanceof HTMLTextAreaElement&&this.currentElement.name}}const R=t=>(e,s={})=>new $(e,s)[t]();var P={get:R("get"),post:R("post"),patch:R("patch"),put:R("put")};class U extends n{constructor(t,e={}){super(t,e);const s=e.value;this.behavior=["auto","instant","smooth"].includes(s)?s:"auto"}scroll(){this.targets[0]?.scrollIntoView({behavior:this.behavior})}}var z={scrollTo:(t=>(e,s={})=>new U(e,s)[t]())("scroll")};const B={attribute:c,class:d,clipboard:p,confirm:E,dataAttribute:A,dialog:x,element:k,form:D,intersection:O,reload:V,request:P,scrollTo:z},G=(t=[])=>0===t.length?Object.values(B).reduce(((t,e)=>({...t,...e})),{}):B.reduce(((e,s)=>t[s]?{...e,...t[s]}:(console.warn(`Action “${s}” not found`),e)),{});var _=G([]);var J=new class{#x={};#M=new WeakMap;#S=new Map;#k;#N=new Map;#F;static get debug(){return t.enabled}static set debug(e){t.enabled=e}constructor(){this.#k=new e(_),this.#F=new r((t=>this.#n(t)),(t=>this.#a(t)))}activate(e={}){const{on:s=document,debug:r=!1}=e;return t.enabled=r,t.log("Initializing…"),this.element=s,this.#F.start("[data-action]"),this.element.querySelectorAll("[data-action]").forEach((t=>this.#n(t))),t.log("…initialized"),this}withActions(s=[]){return t.log("Initializing with actions",s),this.#k=new e(G(s)),this}#n(t){const e=t.dataset.action;if(!e)return;const r=e.split(" ");new Set(e.includes("->")?s.identify({by:e}):[s.getDefault({from:t})]).forEach((t=>this.#D({for:t}))),r.filter((t=>t.includes("@"))).forEach((e=>{const[s]=e.split("->"),[r,i]=s.split("@"),n="window"===r?window:document;this.#q(i,n,t)}));const n=r.filter((t=>t.includes(":"))).map((t=>t.split(":")[1]));n.forEach((e=>{i.setup({for:e,on:t,trigger:()=>this.#k.process({type:e},{on:t,using:e})})}))}#a(t){const e=this.#M.get(t);e&&(e.forEach((e=>{const s=this.#N.get(e);if(s&&(s.delete(t),0===s.size)){const[t,s]=e.split(":"),r=this.#S.get(e);("window"===t?window:document).removeEventListener(s,r),this.#S.delete(e),this.#N.delete(e)}})),this.#M.delete(t))}#D({for:e}){this.#x[e]||(this.element.addEventListener(e,(t=>this.#C(t))),t.log("Added event listener for",e,"to",this.element),this.#x[e]=!0)}#q(t,e,s){const r=`${e===window?"window":"document"}:${t}`;if(this.#M.has(s)||this.#M.set(s,new Set),this.#M.get(s).add(r),this.#N.has(r)||this.#N.set(r,new Set),this.#N.get(r).add(s),!this.#S.has(r)){const s=e=>{const s=this.#N.get(r);s&&s.forEach((s=>{this.#k.process(e,{on:s,using:t})}))};e.addEventListener(t,s),this.#S.set(r,s)}}#C(t){const e=t.target.closest("[data-action]");if(!e)return;const r=s.getDefault({from:e});this.#k.process(t,{on:e,using:r})}};const W=(t=document)=>{window.Attractive&&window.Attractive._initialized||J.activate({element:t})};return"undefined"!=typeof document&&("loading"===document.readyState?document.addEventListener("DOMContentLoaded",(()=>W())):W(),document.addEventListener("turbo:load",(()=>W()))),"undefined"!=typeof window&&(window.Attractive=J),J}));
|
|
@@ -101,6 +101,7 @@
|
|
|
101
101
|
@layer components {
|
|
102
102
|
.events {
|
|
103
103
|
list-style: none;
|
|
104
|
+
margin-block-start: 1rem;
|
|
104
105
|
|
|
105
106
|
li {
|
|
106
107
|
--icon-width: 1.125rem;
|
|
@@ -163,6 +164,64 @@
|
|
|
163
164
|
&[data-status="failed"]::before { color: oklch(60% .2 25); }
|
|
164
165
|
}
|
|
165
166
|
|
|
167
|
+
.filters {
|
|
168
|
+
display: flex;
|
|
169
|
+
align-items: center;
|
|
170
|
+
gap: 2rem;
|
|
171
|
+
|
|
172
|
+
fieldset {
|
|
173
|
+
border: 0;
|
|
174
|
+
|
|
175
|
+
legend {
|
|
176
|
+
font-size: .875rem;
|
|
177
|
+
font-weight: 400;
|
|
178
|
+
color: var(--color-text-muted);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
input[type="radio"] {
|
|
182
|
+
position: absolute;
|
|
183
|
+
clip: rect(0, 0, 0, 0);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.pills {
|
|
187
|
+
display: inline-flex;
|
|
188
|
+
background: var(--color-bg);
|
|
189
|
+
border: 1px solid var(--color-border);
|
|
190
|
+
border-radius: .3rem;
|
|
191
|
+
overflow: clip;
|
|
192
|
+
|
|
193
|
+
.pill {
|
|
194
|
+
display: inline-flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
padding: .25rem .75rem;
|
|
197
|
+
font-size: .875rem;
|
|
198
|
+
font-weight: 400;
|
|
199
|
+
border-inline-start: 1px solid var(--color-border);
|
|
200
|
+
cursor: pointer;
|
|
201
|
+
transition: all ease-in-out 100ms;
|
|
202
|
+
|
|
203
|
+
&:first-child { border-inline-start: 0; }
|
|
204
|
+
|
|
205
|
+
&:has(input:checked),
|
|
206
|
+
&:hover:not(:has(input:checked)) {
|
|
207
|
+
background-color: var(--color-bg-hover);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
select {
|
|
214
|
+
padding: .4rem .75rem;
|
|
215
|
+
font-size: .875rem;
|
|
216
|
+
background: var(--color-bg);
|
|
217
|
+
border: 1px solid var(--color-border);
|
|
218
|
+
border-radius: .25rem;
|
|
219
|
+
cursor: pointer;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
[type=submit] { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border-width: 0; }
|
|
223
|
+
}
|
|
224
|
+
|
|
166
225
|
button,
|
|
167
226
|
.copy-button {
|
|
168
227
|
padding: .125rem;
|
|
@@ -13,11 +13,11 @@ module Fuik
|
|
|
13
13
|
provider: params[:provider],
|
|
14
14
|
event_id: event_id,
|
|
15
15
|
event_type: event_type,
|
|
16
|
-
body:
|
|
16
|
+
body: json_body,
|
|
17
17
|
headers: headers
|
|
18
18
|
)
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
process_later!(webhook_event)
|
|
21
21
|
|
|
22
22
|
head :ok
|
|
23
23
|
rescue Fuik::InvalidSignature
|
|
@@ -30,6 +30,12 @@ module Fuik
|
|
|
30
30
|
|
|
31
31
|
private
|
|
32
32
|
|
|
33
|
+
def json_body
|
|
34
|
+
return request.raw_post unless url_encoded_body?
|
|
35
|
+
|
|
36
|
+
Rack::Utils.parse_nested_query(request.raw_post).to_json
|
|
37
|
+
end
|
|
38
|
+
|
|
33
39
|
def verify_signature!
|
|
34
40
|
return unless should_verify?
|
|
35
41
|
|
|
@@ -47,7 +53,7 @@ module Fuik
|
|
|
47
53
|
@payload ||= begin
|
|
48
54
|
return {} if request.raw_post.blank?
|
|
49
55
|
|
|
50
|
-
JSON.parse(
|
|
56
|
+
JSON.parse(json_body)
|
|
51
57
|
rescue JSON::ParserError
|
|
52
58
|
{}
|
|
53
59
|
end
|
|
@@ -60,6 +66,17 @@ module Fuik
|
|
|
60
66
|
event_class.new(webhook_event).process!
|
|
61
67
|
end
|
|
62
68
|
|
|
69
|
+
def process_later!(webhook_event)
|
|
70
|
+
event_class = event_class_for(webhook_event.provider, webhook_event.event_type)
|
|
71
|
+
return unless event_class
|
|
72
|
+
|
|
73
|
+
WebhookProcessingJob.perform_later(event_class.name, webhook_event)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def url_encoded_body?
|
|
77
|
+
request.media_type == "application/x-www-form-urlencoded"
|
|
78
|
+
end
|
|
79
|
+
|
|
63
80
|
def event_class_for(provider, event_type)
|
|
64
81
|
"#{provider.camelize}::#{event_type.tr("./:-[]", "_").camelize}".safe_constantize
|
|
65
82
|
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Fuik
|
|
4
|
+
class WebhookEvent
|
|
5
|
+
module Filterable
|
|
6
|
+
extend ActiveSupport::Concern
|
|
7
|
+
|
|
8
|
+
class_methods do
|
|
9
|
+
def filtered(params)
|
|
10
|
+
events = all
|
|
11
|
+
events = events.where(status: params[:status]) if params[:status].present?
|
|
12
|
+
events = events.where(provider: params[:provider]) if params[:provider].present?
|
|
13
|
+
events
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def options_for_select
|
|
17
|
+
by_provider_name.pluck(:provider).map { [it.humanize, it] }
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
included do
|
|
22
|
+
scope :by_provider_name, -> { distinct.order(:provider) }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<%= form_tag root_path, method: :get, id: :filters do %>
|
|
2
|
+
<div class="filters">
|
|
3
|
+
<fieldset>
|
|
4
|
+
<legend>Provider</legend>
|
|
5
|
+
|
|
6
|
+
<%= select_tag :provider, options_for_select(Fuik::WebhookEvent.options_for_select, params[:provider]), prompt: "All", data: {action: "form#submit", target: "filters"} %>
|
|
7
|
+
</fieldset>
|
|
8
|
+
|
|
9
|
+
<fieldset>
|
|
10
|
+
<legend>Status</legend>
|
|
11
|
+
|
|
12
|
+
<span class="pills">
|
|
13
|
+
<%= label_tag nil, class: "pill" do %>
|
|
14
|
+
<%= radio_button_tag :status, "", params[:status].blank? || params[:status].empty?, data: {action: "form#submit", target: "filters"} %>
|
|
15
|
+
All
|
|
16
|
+
<% end %>
|
|
17
|
+
|
|
18
|
+
<% Fuik::WebhookEvent.statuses.keys.each do |status| %>
|
|
19
|
+
<%= label_tag nil, class: "pill" do %>
|
|
20
|
+
<%= radio_button_tag :status, status, params[:status] == status, data: {action: "form#submit", target: "filters"} %>
|
|
21
|
+
<%= status.capitalize %>
|
|
22
|
+
<% end %>
|
|
23
|
+
<% end %>
|
|
24
|
+
</span>
|
|
25
|
+
</fieldset>
|
|
26
|
+
|
|
27
|
+
<%= submit_tag "Filter" %>
|
|
28
|
+
</div>
|
|
29
|
+
<% end %>
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
<%= yield :head %>
|
|
9
9
|
|
|
10
10
|
<%= stylesheet_link_tag "fuik/application", media: "all" %>
|
|
11
|
-
<script
|
|
11
|
+
<script src="<%= asset_path("attractivejs-0.12.0.min.js") %>"></script>
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<main>
|
data/lib/fuik/version.rb
CHANGED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class Base < Fuik::Event
|
|
3
|
+
def self.verify!(request)
|
|
4
|
+
secret = Rails.application.credentials.dig(:lettermint, :signing_secret)
|
|
5
|
+
signature_header = request.headers["X-Lettermint-Signature"] || ""
|
|
6
|
+
|
|
7
|
+
elements = {}
|
|
8
|
+
signature_header.split(",").each do |element|
|
|
9
|
+
key, value = element.split("=", 2)
|
|
10
|
+
|
|
11
|
+
elements[key] = value if key && value
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
timestamp = elements["t"]
|
|
15
|
+
signature = elements["v1"]
|
|
16
|
+
|
|
17
|
+
raise Fuik::InvalidSignature if timestamp.blank? || signature.blank?
|
|
18
|
+
|
|
19
|
+
now = Time.current.to_i
|
|
20
|
+
raise Fuik::InvalidSignature if (now - timestamp.to_i).abs > TOLERANCE
|
|
21
|
+
|
|
22
|
+
signed_payload = "#{timestamp}.#{request.raw_post}"
|
|
23
|
+
expected = OpenSSL::HMAC.hexdigest("SHA256", secret, signed_payload)
|
|
24
|
+
|
|
25
|
+
raise Fuik::InvalidSignature unless ActiveSupport::SecurityUtils.secure_compare(expected, signature)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
TOLERANCE = 5.minutes
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class MessageFailed < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.data.recipient # email address that failed
|
|
5
|
+
# payload.data.reason # failure reason (e.g., "A network error occurred.")
|
|
6
|
+
# payload.data.response.status_code # SMTP status code
|
|
7
|
+
# payload.data.subject # email subject
|
|
8
|
+
# payload.data.message_id # message UUID
|
|
9
|
+
# payload.data.tag # message tag (or nil)
|
|
10
|
+
|
|
11
|
+
# TODO: Add business logic
|
|
12
|
+
|
|
13
|
+
@webhook_event.processed!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class MessageHardBounced < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.data.recipient # email address that hard bounced
|
|
5
|
+
# payload.data.response.enhanced_status_code # SMTP enhanced status code (e.g., "5.1.1")
|
|
6
|
+
# payload.data.response.content # SMTP response text (e.g., "User unknown")
|
|
7
|
+
# payload.data.subject # email subject
|
|
8
|
+
# payload.data.message_id # message UUID
|
|
9
|
+
# payload.data.tag # message tag (or nil)
|
|
10
|
+
|
|
11
|
+
# TODO: Add business logic
|
|
12
|
+
|
|
13
|
+
@webhook_event.processed!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class MessageSoftBounced < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.data.recipient # email address that soft bounced
|
|
5
|
+
# payload.data.response.enhanced_status_code # SMTP enhanced status code (e.g., "4.2.2")
|
|
6
|
+
# payload.data.response.content # SMTP response text (e.g., "Mailbox full")
|
|
7
|
+
# payload.data.subject # email subject
|
|
8
|
+
# payload.data.message_id # message UUID
|
|
9
|
+
# payload.data.tag # message tag (or nil)
|
|
10
|
+
|
|
11
|
+
# TODO: Add business logic
|
|
12
|
+
|
|
13
|
+
@webhook_event.processed!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class MessageSuppressed < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.data.recipient # email address that was suppressed
|
|
5
|
+
# payload.data.reason # suppression reason (e.g., "hard_bounce", "spam_complaint")
|
|
6
|
+
# payload.data.subject # email subject
|
|
7
|
+
# payload.data.message_id # message UUID
|
|
8
|
+
# payload.data.tag # message tag (or nil)
|
|
9
|
+
|
|
10
|
+
# TODO: Add business logic
|
|
11
|
+
|
|
12
|
+
@webhook_event.processed!
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class Bounce < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.rcpt # email address that bounced
|
|
5
|
+
# payload.bounce # "hard" or "soft"
|
|
6
|
+
# payload.host # recipient server
|
|
7
|
+
# payload.message # SMTP error message
|
|
8
|
+
# payload.subject # email subject
|
|
9
|
+
# payload.email_id # email identifier
|
|
10
|
+
|
|
11
|
+
# TODO: Add business logic
|
|
12
|
+
|
|
13
|
+
@webhook_event.processed!
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class Reject < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.rcpt # email address that was rejected
|
|
5
|
+
# payload.message # rejection reason
|
|
6
|
+
# payload.subject # email subject
|
|
7
|
+
# payload.email_id # email identifier
|
|
8
|
+
|
|
9
|
+
# TODO: Add business logic
|
|
10
|
+
|
|
11
|
+
@webhook_event.processed!
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class Spam < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.rcpt # email address that complained
|
|
5
|
+
# payload.subject # email subject
|
|
6
|
+
# payload.email_id # email identifier
|
|
7
|
+
|
|
8
|
+
# TODO: Add business logic
|
|
9
|
+
|
|
10
|
+
@webhook_event.processed!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module <%= provider_module_name %>
|
|
2
|
+
class Unsubscribe < Base
|
|
3
|
+
def process!
|
|
4
|
+
# payload.rcpt # email address that unsubscribed
|
|
5
|
+
# payload.subject # email subject
|
|
6
|
+
# payload.email_id # email identifier
|
|
7
|
+
|
|
8
|
+
# TODO: Add business logic
|
|
9
|
+
|
|
10
|
+
@webhook_event.processed!
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fuik
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.12.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rails Designer
|
|
@@ -35,6 +35,7 @@ files:
|
|
|
35
35
|
- MIT-LICENSE
|
|
36
36
|
- README.md
|
|
37
37
|
- Rakefile
|
|
38
|
+
- app/assets/images/fuik/icons/46elks.jpg
|
|
38
39
|
- app/assets/images/fuik/icons/adyen.jpg
|
|
39
40
|
- app/assets/images/fuik/icons/anthropic.jpg
|
|
40
41
|
- app/assets/images/fuik/icons/apple.jpg
|
|
@@ -47,6 +48,7 @@ files:
|
|
|
47
48
|
- app/assets/images/fuik/icons/gitlab.jpg
|
|
48
49
|
- app/assets/images/fuik/icons/google.jpg
|
|
49
50
|
- app/assets/images/fuik/icons/gumroad.jpg
|
|
51
|
+
- app/assets/images/fuik/icons/lettermint.jpg
|
|
50
52
|
- app/assets/images/fuik/icons/linkedin.jpg
|
|
51
53
|
- app/assets/images/fuik/icons/loops.jpg
|
|
52
54
|
- app/assets/images/fuik/icons/mailgun.jpg
|
|
@@ -58,12 +60,14 @@ files:
|
|
|
58
60
|
- app/assets/images/fuik/icons/resend.jpg
|
|
59
61
|
- app/assets/images/fuik/icons/shopify.jpg
|
|
60
62
|
- app/assets/images/fuik/icons/slack.jpg
|
|
63
|
+
- app/assets/images/fuik/icons/smtp2go.jpg
|
|
61
64
|
- app/assets/images/fuik/icons/stripe.jpg
|
|
62
65
|
- app/assets/images/fuik/icons/telegram.jpg
|
|
63
66
|
- app/assets/images/fuik/icons/twitter.jpg
|
|
64
67
|
- app/assets/images/fuik/icons/userlist.jpg
|
|
65
68
|
- app/assets/images/fuik/icons/webhook.svg
|
|
66
69
|
- app/assets/images/fuik/icons/zoom.jpg
|
|
70
|
+
- app/assets/javascript/attractivejs-0.12.0.min.js
|
|
67
71
|
- app/assets/stylesheets/fuik/application.css
|
|
68
72
|
- app/controllers/concerns/fuik/event_type.rb
|
|
69
73
|
- app/controllers/fuik/application_controller.rb
|
|
@@ -73,10 +77,13 @@ files:
|
|
|
73
77
|
- app/helpers/fuik/highlight_helper.rb
|
|
74
78
|
- app/helpers/fuik/icon_helper.rb
|
|
75
79
|
- app/jobs/fuik/application_job.rb
|
|
80
|
+
- app/jobs/fuik/webhook_processing_job.rb
|
|
76
81
|
- app/models/fuik/application_record.rb
|
|
77
82
|
- app/models/fuik/event.rb
|
|
78
83
|
- app/models/fuik/webhook_event.rb
|
|
84
|
+
- app/models/fuik/webhook_event/filterable.rb
|
|
79
85
|
- app/views/fuik/events/_copy_button.html.erb
|
|
86
|
+
- app/views/fuik/events/_filters.html.erb
|
|
80
87
|
- app/views/fuik/events/index.html.erb
|
|
81
88
|
- app/views/fuik/events/show.html.erb
|
|
82
89
|
- app/views/layouts/fuik/application.html.erb
|
|
@@ -96,6 +103,11 @@ files:
|
|
|
96
103
|
- lib/generators/fuik/provider/templates/github/installation_created.rb.tt
|
|
97
104
|
- lib/generators/fuik/provider/templates/github/push.rb.tt
|
|
98
105
|
- lib/generators/fuik/provider/templates/github/star_created.rb.tt
|
|
106
|
+
- lib/generators/fuik/provider/templates/lettermint/base.rb.tt
|
|
107
|
+
- lib/generators/fuik/provider/templates/lettermint/message_failed.rb.tt
|
|
108
|
+
- lib/generators/fuik/provider/templates/lettermint/message_hard_bounced.rb.tt
|
|
109
|
+
- lib/generators/fuik/provider/templates/lettermint/message_soft_bounced.rb.tt
|
|
110
|
+
- lib/generators/fuik/provider/templates/lettermint/message_suppressed.rb.tt
|
|
99
111
|
- lib/generators/fuik/provider/templates/loops/base.rb.tt
|
|
100
112
|
- lib/generators/fuik/provider/templates/loops/contact_unsubscribed.rb.tt
|
|
101
113
|
- lib/generators/fuik/provider/templates/loops/email_hard_bounced.rb.tt
|
|
@@ -110,6 +122,11 @@ files:
|
|
|
110
122
|
- lib/generators/fuik/provider/templates/resend/base.rb.tt
|
|
111
123
|
- lib/generators/fuik/provider/templates/resend/email_bounced.rb.tt
|
|
112
124
|
- lib/generators/fuik/provider/templates/resend/email_complained.rb.tt
|
|
125
|
+
- lib/generators/fuik/provider/templates/smtp2go/base.rb.tt
|
|
126
|
+
- lib/generators/fuik/provider/templates/smtp2go/bounce.rb.tt
|
|
127
|
+
- lib/generators/fuik/provider/templates/smtp2go/reject.rb.tt
|
|
128
|
+
- lib/generators/fuik/provider/templates/smtp2go/spam.rb.tt
|
|
129
|
+
- lib/generators/fuik/provider/templates/smtp2go/unsubscribe.rb.tt
|
|
113
130
|
- lib/generators/fuik/provider/templates/stripe/base.rb.tt
|
|
114
131
|
- lib/generators/fuik/provider/templates/stripe/checkout_session_completed.rb.tt
|
|
115
132
|
- lib/generators/fuik/provider/templates/stripe/customer_subscription_deleted.rb.tt
|
|
@@ -137,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
137
154
|
- !ruby/object:Gem::Version
|
|
138
155
|
version: '0'
|
|
139
156
|
requirements: []
|
|
140
|
-
rubygems_version: 4.0.
|
|
157
|
+
rubygems_version: 4.0.14
|
|
141
158
|
specification_version: 4
|
|
142
159
|
summary: A fish trap for webhooks
|
|
143
160
|
test_files: []
|