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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +107 -0
- data/Rakefile +34 -0
- data/app/assets/config/federal_offense_manifest.js +2 -0
- data/app/assets/javascripts/federal_offense/application.js +7 -0
- data/app/assets/javascripts/federal_offense/cable.js +18 -0
- data/app/assets/javascripts/federal_offense/vendor/action_cable.js +601 -0
- data/app/assets/stylesheets/federal_offense/application.css +33 -0
- data/app/assets/stylesheets/federal_offense/components/article.css +5 -0
- data/app/assets/stylesheets/federal_offense/components/aside.css +9 -0
- data/app/assets/stylesheets/federal_offense/components/empty.css +27 -0
- data/app/assets/stylesheets/federal_offense/components/link.css +4 -0
- data/app/assets/stylesheets/federal_offense/components/list.css +87 -0
- data/app/assets/stylesheets/federal_offense/components/main.css +9 -0
- data/app/assets/stylesheets/federal_offense/components/message.css +13 -0
- data/app/assets/stylesheets/federal_offense/components/nav.css +30 -0
- data/app/assets/stylesheets/federal_offense/components/section.css +6 -0
- data/app/assets/stylesheets/federal_offense/components/table.css +18 -0
- data/app/channels/federal_offense/application_cable/channel.rb +8 -0
- data/app/channels/federal_offense/application_cable/connection.rb +8 -0
- data/app/channels/federal_offense/inbox_channel.rb +16 -0
- data/app/controllers/federal_offense/application_controller.rb +6 -0
- data/app/controllers/federal_offense/messages_controller.rb +55 -0
- data/app/helpers/federal_offense/application_helper.rb +10 -0
- data/app/models/federal_offense/message.rb +141 -0
- data/app/views/federal_offense/messages/index.html.erb +6 -0
- data/app/views/federal_offense/messages/show.html.erb +30 -0
- data/app/views/layouts/federal_offense/application.html.erb +96 -0
- data/config/locales/en.yml +18 -0
- data/config/routes.rb +17 -0
- data/lib/federal_offense.rb +11 -0
- data/lib/federal_offense/engine.rb +57 -0
- data/lib/federal_offense/interceptor.rb +10 -0
- data/lib/federal_offense/version.rb +5 -0
- data/lib/tasks/federal_offense_tasks.rake +9 -0
- 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,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,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,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,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,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,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,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
|