Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
* add Fedora/RH package support

* resolve jocosocial#231, adds timeZoneID to EventData

* resolve jocosocial#165, Home Location not Home Town

* resolve jocosocial#222, seamail foruser is now case insensitive

* resolve jocosocial#133, change disabled feature return code response

* resolve jocosocial#193, adds food & drink forum category

* resolve jocosocial#178, seamail brand name in trunk

* resolve jocosocial#171, add forum favorite indicator
  • Loading branch information
cohoe authored Dec 28, 2023
1 parent 83f226e commit 896c47a
Show file tree
Hide file tree
Showing 12 changed files with 112 additions and 74 deletions.
4 changes: 2 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ let package = Package(
.package(url: "https://github.com/johnsundell/ink.git", from: "0.1.0"),
],
targets: [
.systemLibrary(name: "gd", pkgConfig: "gdlib", providers: [.apt(["libgd-dev"]), .brew(["gd"])]),
.systemLibrary(name: "jpeg", pkgConfig: "libjpeg", providers: [.apt(["libjpeg-dev"]), .brew(["jpeg-turbo"])]),
.systemLibrary(name: "gd", pkgConfig: "gdlib", providers: [.apt(["libgd-dev"]), .brew(["gd"]), .yum(["gd-devel"])]),
.systemLibrary(name: "jpeg", pkgConfig: "libjpeg", providers: [.apt(["libjpeg-dev"]), .brew(["jpeg-turbo"]), .yum(["libjpeg-turbo-devel"])]),
.target(name: "gdOverrides", dependencies: ["gd", "jpeg"], publicHeadersPath: "."),
.executableTarget(
name: "swiftarr",
Expand Down
4 changes: 2 additions & 2 deletions Sources/swiftarr/Controllers/FezController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1082,13 +1082,13 @@ extension FezController {
return user
}
var effectiveUser = user
if effectiveUserParam == "moderator", let modUser = req.userCache.getUser(username: "moderator") {
if effectiveUserParam.lowercased() == "moderator", let modUser = req.userCache.getUser(username: "moderator") {
guard user.accessLevel.hasAccess(.moderator) else {
throw Abort(.forbidden, reason: "Only moderators can access moderator seamail.")
}
effectiveUser = modUser
}
if effectiveUserParam == "twitarrteam", let ttUser = req.userCache.getUser(username: "TwitarrTeam") {
if effectiveUserParam.lowercased() == "twitarrteam", let ttUser = req.userCache.getUser(username: "TwitarrTeam") {
guard user.accessLevel.hasAccess(.twitarrteam) else {
throw Abort(.forbidden, reason: "Only TwitarrTeam members can access TwitarrTeam seamail.")
}
Expand Down
3 changes: 3 additions & 0 deletions Sources/swiftarr/Controllers/Structs/ControllerStructs.swift
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,8 @@ public struct EventData: Content {
var endTime: Date
/// The timezone that the ship is going to be in when the event occurs. Delivered as an abbreviation e.g. "EST".
var timeZone: String
/// The timezone ID that the ship is going to be in when the event occurs. Example: "America/New_York".
var timeZoneID: String
/// The location of the event.
var location: String
/// The event category.
Expand All @@ -359,6 +361,7 @@ extension EventData {
self.startTime = timeZoneChanges.portTimeToDisplayTime(event.startTime)
self.endTime = timeZoneChanges.portTimeToDisplayTime(event.endTime)
self.timeZone = timeZoneChanges.abbrevAtTime(self.startTime)
self.timeZoneID = timeZoneChanges.tzAtTime(self.startTime).identifier
location = event.location
eventType = event.eventType.label
lastUpdateTime = event.updatedAt ?? Date()
Expand Down
6 changes: 5 additions & 1 deletion Sources/swiftarr/Helpers/DisabledSectionMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ struct DisabledAPISectionMiddleware: AsyncMiddleware {
func respond(to request: Request, chainingTo next: AsyncResponder) async throws -> Response {
let features = Settings.shared.disabledFeatures.value
if features[.all]?.contains(featureToCheck) ?? false || features[.all]?.contains(.all) ?? false {
throw Abort(.serviceUnavailable)
// There are a couple StackOverflow posts discussing what code is most appropriate
// for a "correct request but the server refuses to process it" situation like this.
// The 451 Legal Reasons code is so far out out there that I think we're in safe
// position to use that as the code for "we've disabled this feature".
throw Abort(.unavailableForLegalReasons, reason: "This feature has been disabled by the server admins.")
}
return try await next.respond(to: request)
}
Expand Down
40 changes: 21 additions & 19 deletions Sources/swiftarr/Helpers/EventParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ final class EventParser {
default:
break
}
if inEvent {
if inEvent {
// Gather components of this event
eventComponents.append(String(element))
}
Expand Down Expand Up @@ -144,8 +144,8 @@ final class EventParser {
return Event(startTime: start, endTime: end, title: title, description: description, location: location,
eventType: eventType, uid: uid)
}
// Used to remove .ics character escape sequeneces from TEXT value types. The spec specifies different escape sequences for

// Used to remove .ics character escape sequeneces from TEXT value types. The spec specifies different escape sequences for
// text-valued property values than for other value types, or for strings that aren't property values.
func unescapedTextValue(_ value: any StringProtocol) -> String {
return value.replacingOccurrences(of: "&", with: "&")
Expand All @@ -155,26 +155,26 @@ final class EventParser {
.replacingOccurrences(of: "\\n", with: "\n")
.replacingOccurrences(of: "\\N", with: "\n")
}

// A DateFormatter for converting ISO8601 strings of the form "19980119T070000Z". RFC 5545 calls these 'form 2' date strings.
let gmtDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss'Z'"
dateFormatter.timeZone = TimeZone(identifier: "GMT")
return dateFormatter
}()
// From testing: If the dateFormat doesn't have a 'Z' GMT specifier, conversion will fail if the string to convert contains a 'Z'.

// From testing: If the dateFormat doesn't have a 'Z' GMT specifier, conversion will fail if the string to convert contains a 'Z'.
let tzDateFormatter: DateFormatter = {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyyMMdd'T'HHmmss"
dateFormatter.timeZone = TimeZone(identifier: "GMT")
return dateFormatter
}()

// Even if the Sched.com file were to start using floating datetimes (which would solve several timezone problems) we're not
// currently set up to work with them directly. Instead we convert all ics datetimes to Date() objects, and try to get the tz right.
// This also means that 'Form 3' dates (ones with a time zone reference) lose their associated timezone but still indicate the
// This also means that 'Form 3' dates (ones with a time zone reference) lose their associated timezone but still indicate the
// correct time.
func parseDateFromProperty(property keyAndParams: [Substring], value: String) throws -> Date {
for index in 1..<keyAndParams.count {
Expand Down Expand Up @@ -214,16 +214,16 @@ final class EventParser {
throw Abort(.internalServerError, reason: "Event Parser: couldn't parse a date value.")
}
}

// MARK: Validation

/// Takes an .ics Schedule file and compares it against the db. Returns a summary of what would change if the schedule is applied. Shows deleted events, added events,
/// events with modified times, events with changes to their title or description text).
/// events with modified times, events with changes to their title or description text).
///
/// - Parameters:
/// - scheduleFileStr: The contents of an ICS file; usually a sched.com `.ics` export. Conforms to RFC 5545.
/// - db: The connection to a database.
///
///
/// - Returns: `EventUpdateDifferenceData` with info on the events that were modified/added/removed .
func validateEventsInICS(_ scheduleFileStr: String, on db: Database) async throws -> EventUpdateDifferenceData {
let updateEvents = try parse(scheduleFileStr)
Expand Down Expand Up @@ -256,6 +256,7 @@ final class EventParser {
startTime: updated.startTime,
endTime: updated.endTime,
timeZone: "",
timeZoneID: "",
location: updated.location,
eventType: updated.eventType.rawValue,
lastUpdateTime: updated.updatedAt ?? Date(),
Expand All @@ -278,6 +279,7 @@ final class EventParser {
startTime: updated.startTime,
endTime: updated.endTime,
timeZone: "",
timeZoneID: "",
location: updated.location,
eventType: updated.eventType.rawValue,
lastUpdateTime: updated.updatedAt ?? Date(),
Expand All @@ -300,19 +302,19 @@ final class EventParser {

return responseData
}

/// Takes an .ics Schedule file, updates the db with new info from the schedule. Returns a summary of what changed and how (deleted events, added events,
/// events with modified times, events with changes to their title or description text).
/// events with modified times, events with changes to their title or description text).
///
/// - Parameters:
/// - scheduleFileStr: The contents of an ICS file; usually a sched.com `.ics` export. Conforms to RFC 5545.
/// - db: The connection to a database.
/// - forumAuthor: If there are changes and `makeForumPosts` is TRUE, the user to use as the author of the relevant forum posts.
/// - processDeletes: If TRUE (the default) events in the db but not in `scheduleFileStr` will be deleted from the db. Set to FALSE if you are applying a schedule
/// - processDeletes: If TRUE (the default) events in the db but not in `scheduleFileStr` will be deleted from the db. Set to FALSE if you are applying a schedule
/// patch (e.g. a schedule file with a single new event).
/// - makeForumPosts: Adds forum posts to each Event forum's thread announcing the changes that were made to the event.
///
func updateDatabaseFromICS(_ scheduleFileStr: String, on db: Database, forumAuthor: UserHeader,
///
func updateDatabaseFromICS(_ scheduleFileStr: String, on db: Database, forumAuthor: UserHeader,
processDeletes: Bool = true, makeForumPosts: Bool = true) async throws {
let updateEvents = try parse(scheduleFileStr)
let officialCategory = try await Category.query(on: db).filter(\.$title, .custom("ILIKE"), "event%").first()
Expand All @@ -338,7 +340,7 @@ final class EventParser {
text: """
Automatic Notification of Schedule Change: This event has been deleted from the \
schedule. Apologies to those planning on attending.
However, since this is an automatic announcement, it's possible the event got moved or \
rescheduled and it only looks like a delete to me, your automatic server software. \
Check the schedule.
Expand Down Expand Up @@ -436,7 +438,7 @@ final class EventParser {
let newPost = try ForumPost(forum: forum, authorID: forumAuthor.userID,
text: """
Automatic Notification of Schedule Change: This event has changed.
\(changes.contains(.undelete) ? "This event was canceled, but now is un-canceled.\r" : "")\
\(changes.contains(.startTime) ? "Start Time changed\r" : "")\
\(changes.contains(.endTime) ? "End Time changed\r" : "")\
Expand Down
22 changes: 22 additions & 0 deletions Sources/swiftarr/Migrations/Categories.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,25 @@ struct RenameWhereAndWhen: AsyncMigration {
.update()
}
}

struct AddFoodDrinkCategory: AsyncMigration {
/// Required by `Migration` protocol. Inserts the Food & Drink category.
///
/// - Parameter database: A connection to the database, provided automatically.
/// - Returns: Void.
///
func prepare(on database: Database) async throws {
let categories: [Category] = [
.init(title: "Food & Drink", purpose: "Dinner reviews, drink photos, all things consumable.")
]
try await categories.create(on: database)
}

/// Undoes this migration, removing the Food & Drink category if it got created.
///
/// - Parameter database: The database connection.
/// - Returns: Void.
func revert(on database: Database) async throws {
try await Category.query(on: database).filter(\.$title == "Food & Drink").delete()
}
}
13 changes: 9 additions & 4 deletions Sources/swiftarr/Resources/Assets/js/swiftarr.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,11 @@ function setActionButtonsState(tappedButton, state) {
}

// Button handler for buttons that POST on click, spinner while processing, reload/redirect on completion, and display errors inline.
// on Button: data-actionpath="/path/to/POST/to"
// on Button: data-actionpath="/path/to/POST/to"
// data-istoggle="true if it toggles via POST/DELETE of its actionPath"
// data-errordiv="id"
// on error div: class="d-none"
// in error div: <span class="errorText"></span>
// in error div: <span class="errorText"></span>
async function spinnerButtonAction() {
let tappedButton = event.target;
let path = tappedButton.dataset.actionpath;
Expand Down Expand Up @@ -92,6 +92,11 @@ async function spinnerButtonAction() {
break;
case "follow":
tappedButton.closest('[data-eventfavorite]').dataset.eventfavorite = tappedButton.checked ? "true" : "false";
if (tappedButton.checked) {
tappedButton.closest('[data-eventfavorite]').querySelector('.event-favorite-icon').classList.remove('d-none');
} else {
tappedButton.closest('[data-eventfavorite]').querySelector('.event-favorite-icon').classList.add('d-none');
}
break;
case "reload":
location.reload();
Expand Down Expand Up @@ -147,7 +152,7 @@ for (let modal of document.querySelectorAll('.modal')) {
});
}

// When a Delete Modal is shown, stash the ID of the thing being deleted in the Delete btn.
// When a Delete Modal is shown, stash the ID of the thing being deleted in the Delete btn.
document.getElementById('deleteModal')?.addEventListener('show.bs.modal', function (event) {
let postElem = event.relatedTarget.closest('[data-postid]');
let deleteBtn = event.target.querySelector('[data-delete-postid]');
Expand Down Expand Up @@ -571,7 +576,7 @@ function applyFezSearchFilters() {
window.location.href = window.location.href.split("?")[0] + queryString;
}

// Populates username completions for a partial username.
// Populates username completions for a partial username.
let userSearchAPICallTimeout = null;
let userSearch = document.querySelector('input.user-autocomplete');
userSearch?.addEventListener('input', function (event) {
Expand Down
18 changes: 9 additions & 9 deletions Sources/swiftarr/Resources/Views/User/userProfile.html
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
Email: <b>#(profile.email)</b><br>
#endif
#if(profile.homeLocation != ""):
Home Town: <b>#(profile.homeLocation)</b><br>
Home Location: <b>#(profile.homeLocation)</b><br>
#endif
#if(profile.roomNumber != ""):
Room #: <b>#(profile.roomNumber)</b><br>
Expand All @@ -40,7 +40,7 @@
<div class="col col-auto p-1">
<h6><b>About @#(profile.header.username)&#58;</b></h6>
#(profile.about)
</div>
</div>
</div>
#endif
#if(profile.header.userID != trunk.userID):
Expand All @@ -49,11 +49,11 @@ <h6><b>About @#(profile.header.username)&#58;</b></h6>
<div class="col col-12 p-1">
<b>Your private note about @#(profile.header.username) (only visible to you):</b><br>
<textarea class="form-control" maxlength="2000" rows="5" name="noteText" id="noteText" placeholder="">#(profile.note)</textarea>
</div>
</div>
<div class="col col-12">
<div class="alert alert-danger mt-3 d-none" role="alert">
</div>
</div>
</div>
</div>
<div class="col col-auto me-auto">
</div>
<div class="col col-auto">
Expand Down Expand Up @@ -98,8 +98,8 @@ <h5 class="modal-title" id="blockModalTitle">Block User @#(profile.header.userna
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary px-4" data-action="block"
data-actionpath="/user/#(profile.header.userID)/block"
<button type="button" class="btn btn-primary px-4" data-action="block"
data-actionpath="/user/#(profile.header.userID)/block"
data-errordiv="blockDialog_errorDisplay">
Block
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
Expand Down Expand Up @@ -127,8 +127,8 @@ <h5 class="modal-title" id="muteModalTitle">Mute User @#(profile.header.username
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary px-4" autocomplete="off" data-action="mute"
data-actionpath="/user/#(profile.header.userID)/mute"
<button type="button" class="btn btn-primary px-4" autocomplete="off" data-action="mute"
data-actionpath="/user/#(profile.header.userID)/mute"
data-errordiv="muteDialog_errorDisplay">
Mute
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
Expand Down
Loading

0 comments on commit 896c47a

Please sign in to comment.