federal_offense 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +107 -0
  4. data/Rakefile +34 -0
  5. data/app/assets/config/federal_offense_manifest.js +2 -0
  6. data/app/assets/javascripts/federal_offense/application.js +7 -0
  7. data/app/assets/javascripts/federal_offense/cable.js +18 -0
  8. data/app/assets/javascripts/federal_offense/vendor/action_cable.js +601 -0
  9. data/app/assets/stylesheets/federal_offense/application.css +33 -0
  10. data/app/assets/stylesheets/federal_offense/components/article.css +5 -0
  11. data/app/assets/stylesheets/federal_offense/components/aside.css +9 -0
  12. data/app/assets/stylesheets/federal_offense/components/empty.css +27 -0
  13. data/app/assets/stylesheets/federal_offense/components/link.css +4 -0
  14. data/app/assets/stylesheets/federal_offense/components/list.css +87 -0
  15. data/app/assets/stylesheets/federal_offense/components/main.css +9 -0
  16. data/app/assets/stylesheets/federal_offense/components/message.css +13 -0
  17. data/app/assets/stylesheets/federal_offense/components/nav.css +30 -0
  18. data/app/assets/stylesheets/federal_offense/components/section.css +6 -0
  19. data/app/assets/stylesheets/federal_offense/components/table.css +18 -0
  20. data/app/channels/federal_offense/application_cable/channel.rb +8 -0
  21. data/app/channels/federal_offense/application_cable/connection.rb +8 -0
  22. data/app/channels/federal_offense/inbox_channel.rb +16 -0
  23. data/app/controllers/federal_offense/application_controller.rb +6 -0
  24. data/app/controllers/federal_offense/messages_controller.rb +55 -0
  25. data/app/helpers/federal_offense/application_helper.rb +10 -0
  26. data/app/models/federal_offense/message.rb +141 -0
  27. data/app/views/federal_offense/messages/index.html.erb +6 -0
  28. data/app/views/federal_offense/messages/show.html.erb +30 -0
  29. data/app/views/layouts/federal_offense/application.html.erb +96 -0
  30. data/config/locales/en.yml +18 -0
  31. data/config/routes.rb +17 -0
  32. data/lib/federal_offense.rb +11 -0
  33. data/lib/federal_offense/engine.rb +57 -0
  34. data/lib/federal_offense/interceptor.rb +10 -0
  35. data/lib/federal_offense/version.rb +5 -0
  36. data/lib/tasks/federal_offense_tasks.rake +9 -0
  37. metadata +283 -0
@@ -0,0 +1,33 @@
1
+ /*
2
+ * This is a manifest file that'll be compiled into application.css, which will include all the files
3
+ * listed below.
4
+ *
5
+ * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
6
+ * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
7
+ *
8
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
9
+ * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
10
+ * files in this directory. Styles in this file should be added after the last require_* statement.
11
+ * It is generally better to create a new file per style scope.
12
+ *
13
+ *= require_tree .
14
+ *= require_tree ./components
15
+ *= require_self
16
+ */
17
+ body,
18
+ html {
19
+ background-color: #fff;
20
+ height: 100vh;
21
+ margin: 0;
22
+ overflow: hidden;
23
+ padding: 0;
24
+ }
25
+
26
+ body {
27
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;
28
+ font-size: 1rem;
29
+ }
30
+
31
+ html {
32
+ font-size: 12pt;
33
+ }
@@ -0,0 +1,5 @@
1
+ article {
2
+ align-items: stretch;
3
+ flex: 1;
4
+ display: flex;
5
+ }
@@ -0,0 +1,9 @@
1
+ aside {
2
+ display: flex;
3
+ flex: 1;
4
+ }
5
+
6
+ aside:not(:last-child) {
7
+ flex: auto;
8
+ max-width: 300px;
9
+ }
@@ -0,0 +1,27 @@
1
+ .empty {
2
+ align-items: center;
3
+ border-radius: 1rem;
4
+ border: 3px dashed lightgray;
5
+ display: flex;
6
+ flex-direction: column;
7
+ flex: 1;
8
+ justify-content: center;
9
+ margin: 1rem;
10
+ padding: 1rem;
11
+ text-align: center;
12
+ }
13
+
14
+ .empty .empty-title {
15
+ color: gray;
16
+ display: block;
17
+ font-weight: bold;
18
+ }
19
+
20
+ .empty .empty-subtitle {
21
+ color: gray;
22
+ display: block;
23
+ }
24
+
25
+ .empty .empty-title + .empty-subtitle {
26
+ margin-top: 0.8rem;
27
+ }
@@ -0,0 +1,4 @@
1
+ a[disabled] {
2
+ color: lightgray !important;
3
+ cursor: not-allowed;
4
+ }
@@ -0,0 +1,87 @@
1
+ .list {
2
+ display: flex;
3
+ flex-direction: column;
4
+ flex: 1;
5
+ list-style-type: none;
6
+ margin: 0;
7
+ padding: 0;
8
+ }
9
+
10
+ .list .list-item {
11
+ position: relative;
12
+ }
13
+
14
+ /* Link styling */
15
+ .list .list-item .list-item-link {
16
+ color: inherit;
17
+ display: block;
18
+ padding: 1rem 1rem 1rem 2rem;
19
+ text-decoration: none;
20
+ }
21
+
22
+ /* Text content */
23
+ .list .list-item .list-item-title,
24
+ .list .list-item .list-item-subtitle,
25
+ .list .list-item .list-item-content {
26
+ display: block;
27
+ }
28
+
29
+ .list .list-item .list-item-title,
30
+ .list .list-item .list-item-subtitle {
31
+ max-width: 100%;
32
+ text-overflow: ellipsis;
33
+ white-space: nowrap;
34
+ }
35
+
36
+ .list .list-item .list-item-subtitle {
37
+ color: gray;
38
+ font-size: 0.8rem;
39
+ }
40
+
41
+ .list .list-item .list-item-title {
42
+ }
43
+
44
+ .list .list-item .list-item-content {
45
+ font-size: 0.6rem;
46
+ }
47
+
48
+ /* Adjacent list items */
49
+ .list .list-item + .list-item {
50
+ border-top: 1px solid lightgray;
51
+ }
52
+
53
+ /* Status indicators */
54
+ .list .list-item.list-item--unread::before {
55
+ border-radius: 1000px;
56
+ content: "";
57
+ height: 0.5rem;
58
+ left: 1rem;
59
+ overflow: hidden;
60
+ position: absolute;
61
+ top: 50%;
62
+ transform: translate(-50%, -50%);
63
+ width: 0.5rem;
64
+ }
65
+
66
+ .list .list-item.list-item--unread::before {
67
+ background-color: steelblue;
68
+ }
69
+
70
+ /* Active state */
71
+ .list .list-item.list-item--active {
72
+ background-color: steelblue;
73
+ }
74
+
75
+ .list-item.list-item--active.list-item--unread::before {
76
+ background-color: white !important;
77
+ }
78
+
79
+ .list .list-item.list-item--active .list-item-link,
80
+ .list .list-item.list-item--active .list-item-subtitle {
81
+ color: white;
82
+ }
83
+
84
+ .list .list-item.list-item--active + .list-item,
85
+ .list .list-item + .list-item.list-item--active {
86
+ border-top-color: steelblue;
87
+ }
@@ -0,0 +1,9 @@
1
+ main {
2
+ align-items: stretch;
3
+ display: flex;
4
+ flex-direction: column;
5
+ height: 100vh;
6
+ margin: 0;
7
+ overflow: hidden;
8
+ padding: 0;
9
+ }
@@ -0,0 +1,13 @@
1
+ .message-header {
2
+ background-color: whitesmoke;
3
+ border-bottom: 1px solid lightgray;
4
+ display: block;
5
+ padding: 0.8rem 0.6rem;
6
+ }
7
+
8
+ .message-body {
9
+ border-width: 0;
10
+ display: block;
11
+ flex: 1;
12
+ width: 100%;
13
+ }
@@ -0,0 +1,30 @@
1
+ nav {
2
+ border-bottom: 1px solid lightgray;
3
+ display: block;
4
+ margin: 0;
5
+ padding: 0;
6
+ }
7
+
8
+ .nav-items {
9
+ align-items: stretch;
10
+ display: flex;
11
+ list-style-type: none;
12
+ justify-content: space-between;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ .nav-items .nav-item.nav-item--logo {
18
+ font-weight: bold;
19
+ }
20
+
21
+ .nav-items .nav-item + .nav-item {
22
+ border-left: 1px solid lightgray;
23
+ }
24
+
25
+ .nav-items .nav-item a {
26
+ color: inherit;
27
+ display: block;
28
+ padding: 1rem;
29
+ text-decoration: none;
30
+ }
@@ -0,0 +1,6 @@
1
+ section {
2
+ border-left: 1px solid lightgray;
3
+ display: flex;
4
+ flex: 1;
5
+ flex-direction: column;
6
+ }
@@ -0,0 +1,18 @@
1
+ table {
2
+ border-collapse: collapse;
3
+ border-width: 0;
4
+ }
5
+
6
+ table th,
7
+ table td {
8
+ padding: 0.2rem 0.4rem;
9
+ }
10
+
11
+ table th {
12
+ font-size: 0.8rem;
13
+ font-weight: bold;
14
+ text-align: right;
15
+ text-transform: uppercase;
16
+ vertical-align: bottom;
17
+ white-space: nowrap;
18
+ }
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ module ApplicationCable
5
+ class Channel < ActionCable::Channel::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ module ApplicationCable
5
+ class Connection < ActionCable::Connection::Base
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ class InboxChannel < FederalOffense::ApplicationCable::Channel
5
+ def self.broadcast!(message: {reload: true})
6
+ FederalOffense::Engine.cable_server.broadcast(name, message)
7
+ end
8
+
9
+ def subscribed
10
+ stream_from self.class.name
11
+ end
12
+
13
+ def unsubscribed
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ class ApplicationController < ActionController::Base
5
+ end
6
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ class MessagesController < FederalOffense::ApplicationController
5
+ before_action :require_message, only: %i[destroy show]
6
+ after_action :mark_as_read, only: %i[show]
7
+
8
+ def destroy
9
+ message.destroy
10
+ redirect_to messages_path
11
+ end
12
+
13
+ def destroy_all
14
+ FederalOffense::Message.destroy_all
15
+ redirect_to messages_path
16
+ end
17
+
18
+ def index
19
+ end
20
+
21
+ def show
22
+ if params[:raw]
23
+ case params[:type]
24
+ when "html"
25
+ render html: message.html_body
26
+ when "text"
27
+ render plain: message.text_body
28
+ else
29
+ render message.html? ? {html: message.html_body} : {plain: message.text_body}
30
+ end
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def mark_as_read
37
+ message.update(read_at: Time.zone.now.to_i)
38
+ end
39
+
40
+ def message
41
+ return @message if defined? @message
42
+ @message = messages.find { |message| message.id == params[:id] }
43
+ end
44
+ helper_method :message
45
+
46
+ def messages
47
+ @messages ||= FederalOffense::Message.all
48
+ end
49
+ helper_method :messages
50
+
51
+ def require_message
52
+ redirect_to messages_path if message.blank?
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ module ApplicationHelper
5
+ def message_array(input)
6
+ return "(none)" if input.blank?
7
+ Array(input).compact.to_sentence
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FederalOffense
4
+ class Message < OpenStruct
5
+ ROOT = Rails.root.join("tmp", "federal_offense").freeze
6
+
7
+ EMPTY_TEXT_BODY = "No message content"
8
+ EMPTY_HTML_BODY = "<html><body><em>#{EMPTY_TEXT_BODY}</em></body></html>"
9
+ EMPTY_SUBJECT = "(no subject)"
10
+
11
+ class << self
12
+ def all
13
+ Dir[ROOT.join("*.json")].map { |message|
14
+ read(message)
15
+ }.sort_by(&:date)
16
+ end
17
+
18
+ def create(message)
19
+ # Create a unique ID for the message
20
+ timestamp = Time.zone.now.to_i
21
+ components = [timestamp] + Array(message.from) + Array(message.to) + [message.subject, SecureRandom.hex(3)]
22
+ id = components.join(" ")
23
+ id = Digest::SHA1.hexdigest(id)
24
+
25
+ # Prepare the filesystem for the JSON record
26
+ FileUtils.mkdir_p(ROOT)
27
+ path = ROOT.join("#{id}.json")
28
+
29
+ html, text = if message.parts.any?
30
+ [message.html_part&.body, message.text_part&.body]
31
+ else
32
+ is_html = message.content_type.match?(/html/i)
33
+ [
34
+ is_html ? message.body : nil,
35
+ is_html ? nil : message.body,
36
+ ]
37
+ end
38
+
39
+ # Set up some default attributes
40
+ attributes = {
41
+ bcc: message.bcc,
42
+ cc: message.cc,
43
+ date: timestamp,
44
+ from: message.from,
45
+ html: html,
46
+ id: id,
47
+ path: path,
48
+ subject: message.subject,
49
+ text: text,
50
+ to: message.to,
51
+ }
52
+
53
+ new(attributes).tap do |new_message|
54
+ new_message.write
55
+ new_message.save_attachments(message)
56
+ end
57
+ end
58
+
59
+ def destroy_all
60
+ FileUtils.rm_rf(ROOT)
61
+ end
62
+
63
+ private
64
+
65
+ def read(path)
66
+ attributes = File.exist?(path) ? JSON.parse(File.read(path)) : {}
67
+ new(attributes.merge(path: path))
68
+ end
69
+ end
70
+
71
+ delegate :to_json, to: :as_json
72
+
73
+ def as_json(*args)
74
+ @table.as_json(*args)
75
+ end
76
+
77
+ def date
78
+ @date ||= Time.zone.at(super)
79
+ end
80
+
81
+ def destroy
82
+ FileUtils.rm_rf(ROOT.join(id))
83
+ File.unlink(path)
84
+ end
85
+
86
+ def html?
87
+ html.present?
88
+ end
89
+
90
+ def html_body
91
+ (html || {}).fetch("raw_source", EMPTY_HTML_BODY).html_safe # rubocop:disable Rails/OutputSafety
92
+ end
93
+
94
+ def read?
95
+ read_at.present?
96
+ end
97
+
98
+ def save_attachments(message)
99
+ # return unless message.attachments.any?
100
+ # FileUtils.mkdir_p(ROOT.join(id))
101
+
102
+ # message.attachments.each do |attachment|
103
+ # # TODO: Save the attachments if need be, but this is complex and we don't need to sweat it
104
+ # # for now
105
+ # end
106
+ end
107
+
108
+ def subject
109
+ super.presence || EMPTY_SUBJECT
110
+ end
111
+
112
+ def text?
113
+ text.present?
114
+ end
115
+
116
+ def text_body
117
+ (text || {}).fetch("raw_source", EMPTY_TEXT_BODY)
118
+ end
119
+
120
+ def unread?
121
+ !read?
122
+ end
123
+
124
+ def update(attributes)
125
+ write_attributes(attributes)
126
+ write
127
+ end
128
+
129
+ def write
130
+ File.open(path, "w+") do |file|
131
+ file.puts to_json
132
+ end
133
+ end
134
+
135
+ def write_attributes(attributes)
136
+ attributes.each do |key, value|
137
+ send("#{key}=", value)
138
+ end
139
+ end
140
+ end
141
+ end