1# frozen_string_literal: true
2
3require "json"
4require "value_semantics"
5
6module MessageEvent
7 module Emittable
8 def emit(redis=LazyObject.new { REDIS }, stream: "messages")
9 args = to_redis_fields.flat_map { |k, v| [k, v] }
10 redis.xadd(stream, "*", *args).catch do |e|
11 puts "WARN Failed to emit message event: #{e.message}"
12 end
13 end
14 end
15
16 module ValidTel
17 def self.===(value)
18 return value.match?(/\A\+1\d{10}\z/) if value.is_a?(String)
19 return value.to_s.match?(/\A\+1\d{10}\z/) if value.respond_to?(:to_s)
20
21 false
22 end
23 end
24
25 class MessageFields
26 include ValueSemantics.for_attributes {
27 event String
28 timestamp Time, coerce: true
29 from ValidTel
30 to ArrayOf(ValidTel)
31 message_id String
32 body String
33 media_urls ArrayOf(String), coerce: true
34 }
35
36 def self.coerce_timestamp(value)
37 value.is_a?(String) ? Time.parse(value) : value
38 end
39
40 def self.coerce_media_urls(value)
41 value.is_a?(String) ? JSON.parse(value) : value
42 end
43
44 def to_redis_fields
45 {
46 "event" => event,
47 "timestamp" => timestamp.iso8601,
48 "from" => from,
49 "to" => JSON.dump(to),
50 "message_id" => message_id,
51 "has_media" => media_urls.any?.to_s,
52 "body" => body,
53 "media_urls" => JSON.dump(media_urls)
54 }
55 end
56 end
57
58 %w[In Out Thru].each { |n|
59 const_set(n, Class.new(MessageFields) {
60 include Emittable
61
62 const_set(n, lambda { |v|
63 v == n.downcase
64 })
65
66 define_method(:initialize) { |**a|
67 super(**a.merge(event: n.downcase))
68 }
69 })
70 }
71
72 class Delivered
73 include Emittable
74 Delivered = lambda { |v| v == "delivered" }
75 include ValueSemantics.for_attributes {
76 event Delivered
77 timestamp Either(Time, String)
78 message_id String
79 }
80
81 def initialize(**a)
82 super(**a.merge(event: "delivered"))
83 end
84
85 def to_redis_fields = {
86 "event" => event,
87 "timestamp" => timestamp.iso8601,
88 "message_id" => message_id
89 }
90 end
91
92 class Failed
93 include Emittable
94 Failed = lambda { |v| v == "failed" }
95 include ValueSemantics.for_attributes {
96 event Failed
97 timestamp Either(Time, String)
98 message_id String
99 error_code String
100 error_description String
101 }
102
103 def initialize(**a)
104 super(**a.merge(event: "failed"))
105 end
106
107 def to_redis_fields = {
108 "event" => event,
109 "timestamp" => timestamp.is_a?(Time) ? timestamp.iso8601 : timestamp,
110 "message_id" => message_id,
111 "error_code" => error_code,
112 "error_description" => error_description
113 }
114 end
115end