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