diff --git a/Package.swift b/Package.swift index 5488699e3..94965e3f5 100644 --- a/Package.swift +++ b/Package.swift @@ -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", diff --git a/Sources/swiftarr/Controllers/FezController.swift b/Sources/swiftarr/Controllers/FezController.swift index 8923c21e8..f393b6aa7 100644 --- a/Sources/swiftarr/Controllers/FezController.swift +++ b/Sources/swiftarr/Controllers/FezController.swift @@ -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.") } diff --git a/Sources/swiftarr/Controllers/Structs/ControllerStructs.swift b/Sources/swiftarr/Controllers/Structs/ControllerStructs.swift index 617e2c984..c16090c5d 100644 --- a/Sources/swiftarr/Controllers/Structs/ControllerStructs.swift +++ b/Sources/swiftarr/Controllers/Structs/ControllerStructs.swift @@ -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. @@ -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() diff --git a/Sources/swiftarr/Helpers/DisabledSectionMiddleware.swift b/Sources/swiftarr/Helpers/DisabledSectionMiddleware.swift index 1c1e830d0..7cbb9d44b 100644 --- a/Sources/swiftarr/Helpers/DisabledSectionMiddleware.swift +++ b/Sources/swiftarr/Helpers/DisabledSectionMiddleware.swift @@ -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) } diff --git a/Sources/swiftarr/Helpers/EventParser.swift b/Sources/swiftarr/Helpers/EventParser.swift index de9cff5e0..1879200ef 100644 --- a/Sources/swiftarr/Helpers/EventParser.swift +++ b/Sources/swiftarr/Helpers/EventParser.swift @@ -53,7 +53,7 @@ final class EventParser { default: break } - if inEvent { + if inEvent { // Gather components of this event eventComponents.append(String(element)) } @@ -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: "&") @@ -155,7 +155,7 @@ 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() @@ -163,18 +163,18 @@ final class EventParser { 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.. EventUpdateDifferenceData { let updateEvents = try parse(scheduleFileStr) @@ -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(), @@ -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(), @@ -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() @@ -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. @@ -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" : "")\ diff --git a/Sources/swiftarr/Migrations/Categories.swift b/Sources/swiftarr/Migrations/Categories.swift index edd3c9359..d796f7a43 100644 --- a/Sources/swiftarr/Migrations/Categories.swift +++ b/Sources/swiftarr/Migrations/Categories.swift @@ -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() + } +} diff --git a/Sources/swiftarr/Resources/Assets/js/swiftarr.js b/Sources/swiftarr/Resources/Assets/js/swiftarr.js index 905ca4063..d9c1fe083 100644 --- a/Sources/swiftarr/Resources/Assets/js/swiftarr.js +++ b/Sources/swiftarr/Resources/Assets/js/swiftarr.js @@ -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: +// in error div: async function spinnerButtonAction() { let tappedButton = event.target; let path = tappedButton.dataset.actionpath; @@ -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(); @@ -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]'); @@ -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) { diff --git a/Sources/swiftarr/Resources/Views/User/userProfile.html b/Sources/swiftarr/Resources/Views/User/userProfile.html index 0e63da64c..db4ffeda4 100644 --- a/Sources/swiftarr/Resources/Views/User/userProfile.html +++ b/Sources/swiftarr/Resources/Views/User/userProfile.html @@ -28,7 +28,7 @@ Email: #(profile.email)
#endif #if(profile.homeLocation != ""): - Home Town: #(profile.homeLocation)
+ Home Location: #(profile.homeLocation)
#endif #if(profile.roomNumber != ""): Room #: #(profile.roomNumber)
@@ -40,7 +40,7 @@
About @#(profile.header.username):
#(profile.about) -
+ #endif #if(profile.header.userID != trunk.userID): @@ -49,11 +49,11 @@
About @#(profile.header.username):
Your private note about @#(profile.header.username) (only visible to you):
-
+
-
+ +
@@ -98,8 +98,8 @@
-
+
@@ -88,7 +88,7 @@ #(cruiseDay.name) #endfor #endif -
+
#if(isBeforeCruise): @@ -96,7 +96,7 @@
The cruise hasn't started yet. Here are the events planned on the cruise day shown above. -
+
#endif @@ -105,7 +105,7 @@
The cruise has ended. Here are the events that happened on the cruise day shown above. -
+
#endif @@ -118,19 +118,20 @@ #for(event in events):
  • + data-endtime="#(event.endTime)">
    -
    +
    + #(event.title)
    -
    +
    #eventTime(event.startTime, event.endTime)
    @@ -138,7 +139,7 @@ #(event.location)
    -
    +
    #(event.description)
    @@ -148,7 +149,7 @@ #endif Add to Calendar #if(trunk.userIsLoggedIn): - Loading... #endif -
    +
    Could not follow/unfollow event:
    -
    +
  • #endfor @@ -177,11 +178,11 @@
    +
    - #endif - + #endif +