Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
FeedController
100.00% covered (success)
100.00%
65 / 65
100.00% covered (success)
100.00%
8 / 8
11
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 atom
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 rss
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 ical
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 shifts
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
2
 withEtag
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 getNews
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 getShifts
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Engelsystem\Controllers;
6
7use Carbon\CarbonTimeZone;
8use Engelsystem\Helpers\Authenticator;
9use Engelsystem\Helpers\Carbon;
10use Engelsystem\Http\Request;
11use Engelsystem\Http\Response;
12use Engelsystem\Http\UrlGenerator;
13use Engelsystem\Models\News;
14use Engelsystem\Models\Shifts\ShiftEntry;
15use Illuminate\Support\Collection;
16
17class FeedController extends BaseController
18{
19    /** @var array<string, string> */
20    protected array $permissions = [
21        'atom'   => 'atom',
22        'rss'    => 'atom',
23        'ical'   => 'ical',
24        'shifts' => 'shifts_json_export',
25    ];
26
27    public function __construct(
28        protected Authenticator $auth,
29        protected Request $request,
30        protected Response $response,
31        protected UrlGenerator $url,
32    ) {
33    }
34
35    public function atom(): Response
36    {
37        $news = $this->getNews();
38
39        return $this->withEtag($news)
40            ->withHeader('content-type', 'application/atom+xml; charset=utf-8')
41            ->withView('api/atom', ['news' => $news]);
42    }
43
44    public function rss(): Response
45    {
46        $news = $this->getNews();
47
48        return $this->withEtag($news)
49            ->withHeader('content-type', 'application/rss+xml; charset=utf-8')
50            ->withView('api/rss', ['news' => $news]);
51    }
52
53    public function ical(): Response
54    {
55        $shifts = $this->getShifts();
56
57        /* @var string $timezoneTransitionStart */
58        if ($shifts->isNotEmpty()) {
59            $timezoneTransitionStart = $shifts[0]->shift->start->utc()->isoFormat('YYYYMMDDTHHmmss');
60        } else {
61            $timezoneTransitionStart = Carbon::now()->startOfDay()->utc()->isoFormat('YYYYMMDDTHHmmss');
62        }
63
64        return $this->withEtag($shifts)
65            ->withHeader('content-type', 'text/calendar; charset=utf-8')
66            ->withHeader('content-disposition', 'attachment; filename=shifts.ics')
67            ->withView('api/ical', ['shiftEntries' => $shifts, 'timezoneTransitionStart' => $timezoneTransitionStart]);
68    }
69
70    public function shifts(): Response
71    {
72        /** @var Collection|ShiftEntry[] $shiftEntries */
73        $shiftEntries = $this->getShifts();
74        $timeZone = CarbonTimeZone::create(config('timezone'));
75
76        $response = [];
77        foreach ($shiftEntries as $entry) {
78            $shift = $entry->shift;
79            // Data required for the Fahrplan app integration https://github.com/johnjohndoe/engelsystem
80            // See engelsystem-base/src/main/kotlin/info/metadude/kotlin/library/engelsystem/models/Shift.kt
81            // Explicitly typecasts used to stay consistent
82            // ! All attributes not defined in $data might change at any time !
83            $data = [
84                // Name of the shift (type)
85                /** @deprecated, use shifttype_name instead */
86                'name'           => (string) $shift->shiftType->name,
87                // Shift / Talk title
88                'title'          => (string) $shift->title,
89                // Shift description, should be shown after shifttype_description, markdown formatted
90                'description'    => (string) $shift->description,
91
92                'link'           => (string) $this->url->to('/shifts', ['action' => 'view', 'shift_id' => $shift->id]),
93
94                // Users comment, might be empty
95                'Comment'        => (string) $entry->user_comment,
96
97                // Shift id
98                'SID'            => (int) $shift->id,
99
100                // Shift type
101                'shifttype_id'   => (int) $shift->shiftType->id,
102                // General type of the task
103                'shifttype_name' => (string) $shift->shiftType->name,
104                // General description, markdown formatted, might be empty
105                'shifttype_description' => (string) $shift->shiftType->description,
106
107                // Talk URL, mostly empty
108                'URL'            => (string) $shift->url,
109
110                // Location (room) id
111                'RID'            => (int) $shift->location->id,
112                // Location (room) name
113                'Name'           => (string) $shift->location->name,
114                // Location map url, can be empty
115                'map_url'        => (string) $shift->location->map_url,
116
117                // Start timestamp
118                /** @deprecated start_date should be used */
119                'start'          => (int) $shift->start->timestamp,
120                // Start date
121                'start_date'     => (string) $shift->start->toRfc3339String(),
122                // End timestamp
123                /** @deprecated end_date should be used */
124                'end'            => (int) $shift->end->timestamp,
125                // End date
126                'end_date'       => (string) $shift->end->toRfc3339String(),
127
128                // Timezone offset like "+01:00"
129                /** @deprecated should be retrieved from start_date or end_date */
130                'timezone'       => (string) $timeZone->toOffsetName(),
131                // The events timezone like "Europe/Berlin"
132                'event_timezone' => (string) $timeZone->getName(),
133            ];
134
135            $response[] = [
136                // Model data
137                ...$entry->toArray(),
138
139                // Fahrplan app required data
140                ...$data,
141            ];
142        }
143
144        return $this->withEtag($response)
145            ->withAddedHeader('content-type', 'application/json; charset=utf-8')
146            ->withContent(json_encode($response));
147    }
148
149    protected function withEtag(mixed $value): Response
150    {
151        $hash = md5(json_encode($value));
152
153        return $this->response->setEtag($hash);
154    }
155
156    protected function getNews(): Collection
157    {
158        $news = $this->request->has('meetings')
159            ? News::whereIsMeeting((bool) $this->request->get('meetings', false))
160            : News::query();
161        $news
162            ->limit((int) config('display_news'))
163            ->orderByDesc('updated_at');
164
165        return $news->get();
166    }
167
168    protected function getShifts(): Collection
169    {
170        return $this->auth->userFromApi()
171            ->shiftEntries()
172            ->leftJoin('shifts', 'shifts.id', 'shift_entries.shift_id')
173            ->orderBy('shifts.start')
174            ->with(['shift', 'shift.location', 'shift.shiftType'])
175            ->get(['*', 'shift_entries.id']);
176    }
177}