federal_offense 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 (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