--- /dev/null
+<?php
+
+namespace App\Http\Controllers;
+
+use App\Models\ChatBotLog;
+use Illuminate\Http\Request;
+
+class ChatBotLogController extends Controller {
+
+ public function search(Request $request) {
+ $logs = ChatBotLog::with(['channel', 'origin', 'user'])->orderBy('created_at', 'DESC')->limit(50);
+ return $logs->get()->toJson();
+ }
+
+}
$urls[] = $url;
}
+ $url = new SitemapUrl();
+ $url->path = '/horstielog';
+ $url->lastmod = ChatBotLog::latest()->first()->created_at;
+ $url->changefreq = 'daily';
+ $url->priority = 0.5;
+ $urls[] = $url;
+
return response()->view('sitemap', [
'urls' => $urls,
])->header('Content-Type', 'text/xml');
namespace App\Models;
+use Illuminate\Broadcasting\Channel as PublicChannel;
+use Illuminate\Database\Eloquent\BroadcastsEvents;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ChatBotLog extends Model {
+ use BroadcastsEvents;
use HasFactory;
+ public function broadcastOn($event) {
+ return new PublicChannel('ChatBotLog');
+ }
+
+ public function broadcastWith($event) {
+ $this->load(['channel', 'origin', 'user']);
+ }
+
public function channel() {
return $this->belongsTo(Channel::class);
}
'../pages/AlttpSeed'
)}
/>
+ <Route
+ path="horstielog"
+ lazy={() => import(
+ /* webpackChunkName: "horstie" */
+ '../pages/HorstieLog'
+ )}
+ />
<Route
path="locations"
element={<Techniques namespace="locations" type="location" />}
--- /dev/null
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from 'react-bootstrap';
+
+import Icon from '../common/Icon';
+
+const Link = ({ channel }) => {
+ return <Button
+ href={channel.stream_link}
+ rel="noreferer"
+ target="_blank"
+ title={channel.title}
+ variant="outline-twitch"
+ >
+ <Icon.STREAM />
+
+ {channel.short_name || channel.title}
+ </Button>;
+};
+
+Link.propTypes = {
+ channel: PropTypes.shape({
+ short_name: PropTypes.string,
+ stream_link: PropTypes.string,
+ title: PropTypes.string,
+ }),
+};
+
+export default Link;
import { ListGroup } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
+import ChannelLink from '../channel/Link';
import { getUserName } from '../../helpers/User';
const getEntryDate = entry => moment(entry.created_at).fromNow();
const Item = ({ entry = {} }) => {
const { t } = useTranslation();
- return <ListGroup.Item>
+ return <ListGroup.Item className="d-flex justify-content-between">
<div>
<div>
{entry.text}
{getEntryInfo(entry, t)}
</div>
</div>
+ {entry.channel ?
+ <div>
+ <ChannelLink channel={entry.channel} />
+ </div>
+ : null}
</ListGroup.Item>;
};
Item.propTypes = {
entry: PropTypes.shape({
+ channel: PropTypes.shape({}),
created_at: PropTypes.string,
origin: PropTypes.shape({}),
text: PropTypes.string,
import Item from './Item';
-const List = ({ log = [] }) =>
- <ListGroup variant="flush">
- {log ? log.map(entry =>
- <Item key={entry.id} entry={entry} />
- ) : null}
- </ListGroup>;
+class List extends React.Component {
+
+ componentDidMount() {
+ this.timer = setInterval(() => {
+ this.forceUpdate();
+ }, 30000);
+ }
+
+ componentWillUnmount() {
+ clearInterval(this.timer);
+ }
+
+ render() {
+ const { log } = this.props;
+
+ return <ListGroup variant="flush">
+ {log ? log.map(entry =>
+ <Item key={entry.id} entry={entry} />
+ ) : null}
+ </ListGroup>;
+ }
+
+}
List.propTypes = {
log: PropTypes.arrayOf(PropTypes.shape({
})),
};
+List.defaultProps = {
+ log: [],
+};
+
export default List;
import React from 'react';
import { Button } from 'react-bootstrap';
+import Link from '../channel/Link';
import Icon from '../common/Icon';
import { mayEditRestream } from '../../helpers/permissions';
import { useUser } from '../../hooks/user';
const { user } = useUser();
return <div className="episode-channel text-nowrap">
- <Button
- href={channel.stream_link}
- rel="noreferer"
- target="_blank"
- title={channel.title}
- variant="outline-twitch"
- >
- <Icon.STREAM />
- {' '}
- {channel.short_name || channel.title}
- </Button>
+ <Link channel={channel} />
{onEditRestream && mayEditRestream(user, episode, channel) ?
<Button
className="ms-1"
--- /dev/null
+import axios from 'axios';
+import React from 'react';
+import { Container } from 'react-bootstrap';
+import { Helmet } from 'react-helmet';
+
+import List from '../components/chat-bot-logs/List';
+import ErrorBoundary from '../components/common/ErrorBoundary';
+import ErrorMessage from '../components/common/ErrorMessage';
+import Loading from '../components/common/Loading';
+
+export const Component = () => {
+ const [error, setError] = React.useState(null);
+ const [loading, setLoading] = React.useState(true);
+ const [log, setLog] = React.useState([]);
+
+ React.useEffect(() => {
+ const ctrl = new AbortController();
+ if (!log.length) {
+ setLoading(true);
+ }
+ axios
+ .get(`/api/chatbotlogs/`, {
+ signal: ctrl.signal
+ })
+ .then(response => {
+ setError(null);
+ setLoading(false);
+ setLog(response.data);
+ })
+ .catch(error => {
+ if (!axios.isCancel(error)) {
+ setError(error);
+ setLoading(false);
+ setLog([]);
+ }
+ });
+ window.Echo.channel(`ChatBotLog`)
+ .listen('.ChatBotLogCreated', (e) => {
+ setLog(l => [e.model, ...l]);
+ });
+ return () => {
+ ctrl.abort();
+ window.Echo.leave(`ChatBotLog`);
+ };
+ }, []);
+
+ if (loading) {
+ return <Loading />;
+ }
+
+ if (error) {
+ return <ErrorMessage error={error} />;
+ }
+
+ return <Container>
+ <h1>Horstie Log</h1>
+ <Helmet>
+ <title>Horstie Log</title>
+ </Helmet>
+ <ErrorBoundary>
+ <List log={log} />
+ </ErrorBoundary>
+ </Container>;
+};
Route::get('guessing-game-monitor/{key}', 'App\Http\Controllers\ChannelController@getGuessingGameMonitor');
+Route::get('chatbotlogs', 'App\Http\Controllers\ChatBotLogController@search');
+
Route::get('content', 'App\Http\Controllers\TechniqueController@search');
Route::get('content/{tech:name}', 'App\Http\Controllers\TechniqueController@single');
Route::put('content/{content}', 'App\Http\Controllers\TechniqueController@update');